1use std::collections::HashMap;
2use std::fs;
3use std::path::{Path, PathBuf};
4use std::sync::{Arc, Mutex};
5use thiserror::Error;
6
7#[derive(Debug, Error)]
8pub enum TokenStoreError {
9 #[error("Token not found for key: {0}")]
10 NotFound(String),
11 #[error("Failed to store token: {0}")]
12 StoreFailed(String),
13 #[error("Failed to delete token: {0}")]
14 DeleteFailed(String),
15 #[error("IO error: {0}")]
16 IoError(String),
17 #[error("Keyring backend unavailable: {0}\n\nTo resolve this:\n 1. Unlock your OS keyring/keychain (e.g., login to your desktop environment)\n 2. OR use file-based storage: export SLACKRS_TOKEN_STORE=file")]
18 KeyringUnavailable(String),
19 #[error("Invalid token store backend '{0}'. Valid options: 'keyring', 'file'")]
20 InvalidBackend(String),
21}
22
23pub type Result<T> = std::result::Result<T, TokenStoreError>;
24
25pub trait TokenStore: Send + Sync {
27 fn set(&self, key: &str, token: &str) -> Result<()>;
29
30 fn get(&self, key: &str) -> Result<String>;
32
33 fn delete(&self, key: &str) -> Result<()>;
35
36 fn exists(&self, key: &str) -> bool;
38}
39
40#[derive(Debug, Clone)]
42pub struct InMemoryTokenStore {
43 tokens: Arc<Mutex<HashMap<String, String>>>,
44}
45
46impl InMemoryTokenStore {
47 pub fn new() -> Self {
48 Self {
49 tokens: Arc::new(Mutex::new(HashMap::new())),
50 }
51 }
52}
53
54impl Default for InMemoryTokenStore {
55 fn default() -> Self {
56 Self::new()
57 }
58}
59
60impl TokenStore for InMemoryTokenStore {
61 fn set(&self, key: &str, token: &str) -> Result<()> {
62 let mut tokens = self.tokens.lock().unwrap();
63 tokens.insert(key.to_string(), token.to_string());
64 Ok(())
65 }
66
67 fn get(&self, key: &str) -> Result<String> {
68 let tokens = self.tokens.lock().unwrap();
69 tokens
70 .get(key)
71 .cloned()
72 .ok_or_else(|| TokenStoreError::NotFound(key.to_string()))
73 }
74
75 fn delete(&self, key: &str) -> Result<()> {
76 let mut tokens = self.tokens.lock().unwrap();
77 tokens
78 .remove(key)
79 .ok_or_else(|| TokenStoreError::NotFound(key.to_string()))?;
80 Ok(())
81 }
82
83 fn exists(&self, key: &str) -> bool {
84 let tokens = self.tokens.lock().unwrap();
85 tokens.contains_key(key)
86 }
87}
88
89#[derive(Debug, Clone)]
92pub struct FileTokenStore {
93 file_path: PathBuf,
94 tokens: Arc<Mutex<HashMap<String, String>>>,
95}
96
97impl FileTokenStore {
98 pub fn new() -> Result<Self> {
100 let file_path = Self::default_path()?;
101 Self::with_path(file_path)
102 }
103
104 pub fn with_path(file_path: PathBuf) -> Result<Self> {
106 if let Some(parent) = file_path.parent() {
108 fs::create_dir_all(parent).map_err(|e| {
109 TokenStoreError::IoError(format!("Failed to create directory: {}", e))
110 })?;
111 }
112
113 let tokens = if file_path.exists() {
115 Self::load_tokens(&file_path)?
116 } else {
117 HashMap::new()
118 };
119
120 Ok(Self {
121 file_path,
122 tokens: Arc::new(Mutex::new(tokens)),
123 })
124 }
125
126 pub fn default_path() -> Result<PathBuf> {
129 if let Ok(path) = std::env::var("SLACK_RS_TOKENS_PATH") {
131 return Ok(PathBuf::from(path));
132 }
133
134 let home = directories::BaseDirs::new()
136 .ok_or_else(|| {
137 TokenStoreError::IoError("Failed to determine home directory".to_string())
138 })?
139 .home_dir()
140 .to_path_buf();
141
142 let config_dir = home.join(".config").join("slack-rs");
144 Ok(config_dir.join("tokens.json"))
145 }
146
147 fn load_tokens(path: &Path) -> Result<HashMap<String, String>> {
149 let content = fs::read_to_string(path)
150 .map_err(|e| TokenStoreError::IoError(format!("Failed to read tokens file: {}", e)))?;
151
152 serde_json::from_str(&content)
153 .map_err(|e| TokenStoreError::IoError(format!("Failed to parse tokens file: {}", e)))
154 }
155
156 fn save_tokens(&self) -> Result<()> {
158 let tokens = self.tokens.lock().unwrap();
159 let content = serde_json::to_string_pretty(&*tokens).map_err(|e| {
160 TokenStoreError::StoreFailed(format!("Failed to serialize tokens: {}", e))
161 })?;
162
163 fs::write(&self.file_path, content).map_err(|e| {
165 TokenStoreError::StoreFailed(format!("Failed to write tokens file: {}", e))
166 })?;
167
168 #[cfg(unix)]
170 {
171 use std::os::unix::fs::PermissionsExt;
172 let permissions = fs::Permissions::from_mode(0o600);
173 fs::set_permissions(&self.file_path, permissions).map_err(|e| {
174 TokenStoreError::StoreFailed(format!("Failed to set file permissions: {}", e))
175 })?;
176 }
177
178 Ok(())
179 }
180}
181
182impl Default for FileTokenStore {
183 fn default() -> Self {
184 Self::new().expect("Failed to create FileTokenStore")
185 }
186}
187
188impl TokenStore for FileTokenStore {
189 fn set(&self, key: &str, token: &str) -> Result<()> {
190 let mut tokens = self.tokens.lock().unwrap();
191 tokens.insert(key.to_string(), token.to_string());
192 drop(tokens); self.save_tokens()
194 }
195
196 fn get(&self, key: &str) -> Result<String> {
197 let tokens = self.tokens.lock().unwrap();
198 tokens
199 .get(key)
200 .cloned()
201 .ok_or_else(|| TokenStoreError::NotFound(key.to_string()))
202 }
203
204 fn delete(&self, key: &str) -> Result<()> {
205 let mut tokens = self.tokens.lock().unwrap();
206 tokens
207 .remove(key)
208 .ok_or_else(|| TokenStoreError::NotFound(key.to_string()))?;
209 drop(tokens); self.save_tokens()
211 }
212
213 fn exists(&self, key: &str) -> bool {
214 let tokens = self.tokens.lock().unwrap();
215 tokens.contains_key(key)
216 }
217}
218
219pub struct KeyringTokenStore {
221 service: String,
222}
223
224impl KeyringTokenStore {
225 pub fn new(service: impl Into<String>) -> Self {
228 Self {
229 service: service.into(),
230 }
231 }
232
233 pub fn default_service() -> Self {
236 Self {
237 service: "slack-rs".to_string(),
238 }
239 }
240}
241
242impl TokenStore for KeyringTokenStore {
243 fn set(&self, key: &str, token: &str) -> Result<()> {
244 let entry = keyring::Entry::new(&self.service, key)
245 .map_err(|e| TokenStoreError::StoreFailed(e.to_string()))?;
246 entry
247 .set_password(token)
248 .map_err(|e| TokenStoreError::StoreFailed(e.to_string()))?;
249 Ok(())
250 }
251
252 fn get(&self, key: &str) -> Result<String> {
253 let entry = keyring::Entry::new(&self.service, key)
254 .map_err(|e| TokenStoreError::NotFound(e.to_string()))?;
255 entry
256 .get_password()
257 .map_err(|_| TokenStoreError::NotFound(key.to_string()))
258 }
259
260 fn delete(&self, key: &str) -> Result<()> {
261 let entry = keyring::Entry::new(&self.service, key)
262 .map_err(|e| TokenStoreError::DeleteFailed(e.to_string()))?;
263 entry
264 .delete_credential()
265 .map_err(|e| TokenStoreError::DeleteFailed(e.to_string()))?;
266 Ok(())
267 }
268
269 fn exists(&self, key: &str) -> bool {
270 if let Ok(entry) = keyring::Entry::new(&self.service, key) {
271 entry.get_password().is_ok()
272 } else {
273 false
274 }
275 }
276}
277
278pub fn make_token_key(team_id: &str, user_id: &str) -> String {
280 format!("{}:{}", team_id, user_id)
281}
282
283pub fn make_oauth_client_secret_key(profile_name: &str) -> String {
285 format!("oauth-client-secret:{}", profile_name)
286}
287
288pub fn store_oauth_client_secret(
290 token_store: &dyn TokenStore,
291 profile_name: &str,
292 client_secret: &str,
293) -> Result<()> {
294 let key = make_oauth_client_secret_key(profile_name);
295 token_store.set(&key, client_secret)
296}
297
298pub fn get_oauth_client_secret(token_store: &dyn TokenStore, profile_name: &str) -> Result<String> {
300 let key = make_oauth_client_secret_key(profile_name);
301 token_store.get(&key)
302}
303
304pub fn delete_oauth_client_secret(token_store: &dyn TokenStore, profile_name: &str) -> Result<()> {
306 let key = make_oauth_client_secret_key(profile_name);
307 token_store.delete(&key)
308}
309
310#[derive(Debug, Clone, PartialEq, Eq)]
312pub enum TokenStoreBackend {
313 Keyring,
314 File,
315}
316
317impl std::str::FromStr for TokenStoreBackend {
318 type Err = TokenStoreError;
319
320 fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
321 match s.to_lowercase().as_str() {
322 "keyring" => Ok(TokenStoreBackend::Keyring),
323 "file" => Ok(TokenStoreBackend::File),
324 _ => Err(TokenStoreError::InvalidBackend(s.to_string())),
325 }
326 }
327}
328
329pub fn resolve_token_store_backend() -> Result<TokenStoreBackend> {
337 match std::env::var("SLACKRS_TOKEN_STORE") {
338 Ok(value) => value.parse(),
339 Err(_) => Ok(TokenStoreBackend::Keyring), }
341}
342
343pub fn create_token_store() -> Result<Box<dyn TokenStore>> {
352 let backend = resolve_token_store_backend()?;
353
354 match backend {
355 TokenStoreBackend::Keyring => {
356 let store = KeyringTokenStore::default_service();
358
359 let test_key = "__slackrs_keyring_test__";
362
363 match store.set(test_key, "test") {
365 Ok(_) => {
366 let _ = store.delete(test_key); Ok(Box::new(store))
368 }
369 Err(e) => {
370 Err(TokenStoreError::KeyringUnavailable(e.to_string()))
372 }
373 }
374 }
375 TokenStoreBackend::File => {
376 let store = FileTokenStore::new()?;
377 Ok(Box::new(store))
378 }
379 }
380}
381
382#[cfg(test)]
383mod tests {
384 use super::*;
385
386 #[test]
387 fn test_in_memory_token_store_set_get() {
388 let store = InMemoryTokenStore::new();
389 let key = "T123:U456";
390 let token = "xoxb-test-token";
391
392 store.set(key, token).unwrap();
393 assert_eq!(store.get(key).unwrap(), token);
394 }
395
396 #[test]
397 fn test_in_memory_token_store_delete() {
398 let store = InMemoryTokenStore::new();
399 let key = "T123:U456";
400 let token = "xoxb-test-token";
401
402 store.set(key, token).unwrap();
403 assert!(store.exists(key));
404
405 store.delete(key).unwrap();
406 assert!(!store.exists(key));
407 assert!(store.get(key).is_err());
408 }
409
410 #[test]
411 fn test_in_memory_token_store_not_found() {
412 let store = InMemoryTokenStore::new();
413 let result = store.get("nonexistent");
414 assert!(result.is_err());
415 match result {
416 Err(TokenStoreError::NotFound(_)) => {}
417 _ => panic!("Expected NotFound error"),
418 }
419 }
420
421 #[test]
422 fn test_in_memory_token_store_exists() {
423 let store = InMemoryTokenStore::new();
424 let key = "T123:U456";
425
426 assert!(!store.exists(key));
427 store.set(key, "token").unwrap();
428 assert!(store.exists(key));
429 }
430
431 #[test]
432 fn test_make_token_key() {
433 let key = make_token_key("T123", "U456");
434 assert_eq!(key, "T123:U456");
435 }
436
437 #[test]
438 fn test_in_memory_token_store_multiple_keys() {
439 let store = InMemoryTokenStore::new();
440
441 store.set("T1:U1", "token1").unwrap();
442 store.set("T2:U2", "token2").unwrap();
443
444 assert_eq!(store.get("T1:U1").unwrap(), "token1");
445 assert_eq!(store.get("T2:U2").unwrap(), "token2");
446 }
447
448 #[test]
449 fn test_keyring_token_store_default_service() {
450 let store = KeyringTokenStore::default_service();
451 assert_eq!(store.service, "slack-rs");
452 }
453
454 #[test]
455 fn test_make_oauth_client_secret_key() {
456 let key = make_oauth_client_secret_key("default");
457 assert_eq!(key, "oauth-client-secret:default");
458 }
459
460 #[test]
461 fn test_store_and_get_oauth_client_secret() {
462 let store = InMemoryTokenStore::new();
463 let profile_name = "test-profile";
464 let client_secret = "test-secret-123";
465
466 store_oauth_client_secret(&store, profile_name, client_secret).unwrap();
467 let retrieved = get_oauth_client_secret(&store, profile_name).unwrap();
468 assert_eq!(retrieved, client_secret);
469 }
470
471 #[test]
472 fn test_delete_oauth_client_secret() {
473 let store = InMemoryTokenStore::new();
474 let profile_name = "test-profile";
475 let client_secret = "test-secret-123";
476
477 store_oauth_client_secret(&store, profile_name, client_secret).unwrap();
478 assert!(get_oauth_client_secret(&store, profile_name).is_ok());
479
480 delete_oauth_client_secret(&store, profile_name).unwrap();
481 assert!(get_oauth_client_secret(&store, profile_name).is_err());
482 }
483
484 #[test]
485 fn test_file_token_store_set_get() {
486 use tempfile::TempDir;
487
488 let temp_dir = TempDir::new().unwrap();
489 let file_path = temp_dir.path().join("tokens.json");
490 let store = FileTokenStore::with_path(file_path.clone()).unwrap();
491
492 let key = "T123:U456";
493 let token = "xoxb-test-token";
494
495 store.set(key, token).unwrap();
496 assert_eq!(store.get(key).unwrap(), token);
497
498 assert!(file_path.exists());
500
501 #[cfg(unix)]
503 {
504 use std::os::unix::fs::PermissionsExt;
505 let metadata = fs::metadata(&file_path).unwrap();
506 let permissions = metadata.permissions();
507 assert_eq!(permissions.mode() & 0o777, 0o600);
508 }
509 }
510
511 #[test]
512 fn test_file_token_store_delete() {
513 use tempfile::TempDir;
514
515 let temp_dir = TempDir::new().unwrap();
516 let file_path = temp_dir.path().join("tokens.json");
517 let store = FileTokenStore::with_path(file_path).unwrap();
518
519 let key = "T123:U456";
520 let token = "xoxb-test-token";
521
522 store.set(key, token).unwrap();
523 assert!(store.exists(key));
524
525 store.delete(key).unwrap();
526 assert!(!store.exists(key));
527 assert!(store.get(key).is_err());
528 }
529
530 #[test]
531 fn test_file_token_store_persistence() {
532 use tempfile::TempDir;
533
534 let temp_dir = TempDir::new().unwrap();
535 let file_path = temp_dir.path().join("tokens.json");
536
537 {
539 let store = FileTokenStore::with_path(file_path.clone()).unwrap();
540 store.set("T123:U456", "xoxb-test-token").unwrap();
541 }
542
543 {
545 let store = FileTokenStore::with_path(file_path).unwrap();
546 assert_eq!(store.get("T123:U456").unwrap(), "xoxb-test-token");
547 }
548 }
549
550 #[test]
551 fn test_file_token_store_multiple_keys() {
552 use tempfile::TempDir;
553
554 let temp_dir = TempDir::new().unwrap();
555 let file_path = temp_dir.path().join("tokens.json");
556 let store = FileTokenStore::with_path(file_path).unwrap();
557
558 store.set("T1:U1", "token1").unwrap();
559 store.set("T2:U2", "token2").unwrap();
560 store
561 .set("oauth-client-secret:default", "secret123")
562 .unwrap();
563
564 assert_eq!(store.get("T1:U1").unwrap(), "token1");
565 assert_eq!(store.get("T2:U2").unwrap(), "token2");
566 assert_eq!(
567 store.get("oauth-client-secret:default").unwrap(),
568 "secret123"
569 );
570 }
571
572 #[test]
573 fn test_file_token_store_not_found() {
574 use tempfile::TempDir;
575
576 let temp_dir = TempDir::new().unwrap();
577 let file_path = temp_dir.path().join("tokens.json");
578 let store = FileTokenStore::with_path(file_path).unwrap();
579
580 let result = store.get("nonexistent");
581 assert!(result.is_err());
582 match result {
583 Err(TokenStoreError::NotFound(_)) => {}
584 _ => panic!("Expected NotFound error"),
585 }
586 }
587
588 #[test]
589 #[serial_test::serial]
590 fn test_resolve_token_store_backend_default() {
591 std::env::remove_var("SLACKRS_TOKEN_STORE");
593
594 let backend = resolve_token_store_backend().unwrap();
595 assert_eq!(backend, TokenStoreBackend::Keyring);
596 }
597
598 #[test]
599 #[serial_test::serial]
600 fn test_resolve_token_store_backend_keyring() {
601 std::env::set_var("SLACKRS_TOKEN_STORE", "keyring");
602
603 let backend = resolve_token_store_backend().unwrap();
604 assert_eq!(backend, TokenStoreBackend::Keyring);
605
606 std::env::remove_var("SLACKRS_TOKEN_STORE");
607 }
608
609 #[test]
610 #[serial_test::serial]
611 fn test_resolve_token_store_backend_file() {
612 std::env::set_var("SLACKRS_TOKEN_STORE", "file");
613
614 let backend = resolve_token_store_backend().unwrap();
615 assert_eq!(backend, TokenStoreBackend::File);
616
617 std::env::remove_var("SLACKRS_TOKEN_STORE");
618 }
619
620 #[test]
621 #[serial_test::serial]
622 fn test_resolve_token_store_backend_case_insensitive() {
623 std::env::set_var("SLACKRS_TOKEN_STORE", "KEYRING");
624 assert_eq!(
625 resolve_token_store_backend().unwrap(),
626 TokenStoreBackend::Keyring
627 );
628
629 std::env::set_var("SLACKRS_TOKEN_STORE", "File");
630 assert_eq!(
631 resolve_token_store_backend().unwrap(),
632 TokenStoreBackend::File
633 );
634
635 std::env::remove_var("SLACKRS_TOKEN_STORE");
636 }
637
638 #[test]
639 #[serial_test::serial]
640 fn test_resolve_token_store_backend_invalid() {
641 std::env::set_var("SLACKRS_TOKEN_STORE", "invalid");
642
643 let result = resolve_token_store_backend();
644 assert!(result.is_err());
645 match result {
646 Err(TokenStoreError::InvalidBackend(backend)) => {
647 assert_eq!(backend, "invalid");
648 }
649 _ => panic!("Expected InvalidBackend error"),
650 }
651
652 std::env::remove_var("SLACKRS_TOKEN_STORE");
653 }
654
655 #[test]
656 #[serial_test::serial]
657 fn test_create_token_store_file_backend() {
658 use tempfile::TempDir;
659
660 let temp_dir = TempDir::new().unwrap();
661 let tokens_path = temp_dir.path().join("tokens.json");
662 std::env::set_var("SLACK_RS_TOKENS_PATH", tokens_path.to_str().unwrap());
663 std::env::set_var("SLACKRS_TOKEN_STORE", "file");
664
665 let store = create_token_store().unwrap();
666
667 store.set("test_key", "test_value").unwrap();
669 assert_eq!(store.get("test_key").unwrap(), "test_value");
670
671 std::env::remove_var("SLACKRS_TOKEN_STORE");
672 std::env::remove_var("SLACK_RS_TOKENS_PATH");
673 }
674
675 #[test]
676 fn test_token_store_backend_parse() {
677 use std::str::FromStr;
678
679 assert_eq!(
680 TokenStoreBackend::from_str("keyring").unwrap(),
681 TokenStoreBackend::Keyring
682 );
683 assert_eq!(
684 TokenStoreBackend::from_str("file").unwrap(),
685 TokenStoreBackend::File
686 );
687 assert_eq!(
688 TokenStoreBackend::from_str("KEYRING").unwrap(),
689 TokenStoreBackend::Keyring
690 );
691 assert!(TokenStoreBackend::from_str("invalid").is_err());
692 }
693
694 #[test]
695 #[serial_test::serial]
696 fn test_keyring_unavailable_error_message() {
697 let err = TokenStoreError::KeyringUnavailable("test error".to_string());
699 let err_msg = err.to_string();
700
701 assert!(err_msg.contains("Keyring backend unavailable"));
703 assert!(err_msg.contains("SLACKRS_TOKEN_STORE=file"));
704 assert!(err_msg.contains("Unlock your OS keyring"));
705 }
706
707 #[test]
708 fn test_invalid_backend_error_message() {
709 let err = TokenStoreError::InvalidBackend("badvalue".to_string());
710 let err_msg = err.to_string();
711
712 assert!(err_msg.contains("Invalid token store backend 'badvalue'"));
714 assert!(err_msg.contains("keyring"));
715 assert!(err_msg.contains("file"));
716 }
717
718 #[test]
728 #[serial_test::serial]
729 fn test_keyring_locked_interaction_required() {
730 std::env::remove_var("SLACKRS_TOKEN_STORE");
732 std::env::remove_var("SLACK_RS_TOKENS_PATH");
733
734 let result = create_token_store();
737
738 match result {
741 Ok(_) => {
742 }
745 Err(TokenStoreError::KeyringUnavailable(msg)) => {
746 let err_str = TokenStoreError::KeyringUnavailable(msg.clone()).to_string();
748 assert!(
749 err_str.contains("SLACKRS_TOKEN_STORE=file"),
750 "Error should suggest file fallback: {}",
751 err_str
752 );
753 assert!(
754 err_str.contains("Unlock your OS keyring") || err_str.contains("keyring"),
755 "Error should mention keyring: {}",
756 err_str
757 );
758 }
759 Err(e) => {
760 panic!("Unexpected error type: {:?}", e);
761 }
762 }
763 }
764
765 #[test]
768 #[serial_test::serial]
769 fn test_file_mode_fallback_when_keyring_unavailable() {
770 use tempfile::TempDir;
771
772 let temp_dir = TempDir::new().unwrap();
773 let tokens_path = temp_dir.path().join("tokens.json");
774
775 std::env::set_var("SLACK_RS_TOKENS_PATH", tokens_path.to_str().unwrap());
777 std::env::set_var("SLACKRS_TOKEN_STORE", "file");
778
779 let store = create_token_store().expect("File backend should work");
781
782 store.set("test", "value").unwrap();
784 assert_eq!(store.get("test").unwrap(), "value");
785
786 std::env::remove_var("SLACKRS_TOKEN_STORE");
787 std::env::remove_var("SLACK_RS_TOKENS_PATH");
788 }
789
790 #[test]
793 #[serial_test::serial]
794 fn test_file_mode_uses_existing_path_and_key_format() {
795 use tempfile::TempDir;
796
797 let temp_dir = TempDir::new().unwrap();
798 let tokens_path = temp_dir.path().join("tokens.json");
799
800 std::env::set_var("SLACK_RS_TOKENS_PATH", tokens_path.to_str().unwrap());
802 std::env::set_var("SLACKRS_TOKEN_STORE", "file");
803
804 let store = create_token_store().expect("File backend should work");
805
806 let token_key = make_token_key("T123", "U456");
808 assert_eq!(token_key, "T123:U456");
809 store.set(&token_key, "xoxb-test-token").unwrap();
810 assert_eq!(store.get(&token_key).unwrap(), "xoxb-test-token");
811
812 let secret_key = make_oauth_client_secret_key("default");
814 assert_eq!(secret_key, "oauth-client-secret:default");
815 store.set(&secret_key, "test-secret").unwrap();
816 assert_eq!(store.get(&secret_key).unwrap(), "test-secret");
817
818 assert!(tokens_path.exists());
820 let content = std::fs::read_to_string(&tokens_path).unwrap();
821 assert!(content.contains("T123:U456"));
822 assert!(content.contains("oauth-client-secret:default"));
823
824 std::env::remove_var("SLACKRS_TOKEN_STORE");
825 std::env::remove_var("SLACK_RS_TOKENS_PATH");
826 }
827
828 #[test]
831 #[serial_test::serial]
832 fn test_file_token_store_default_path() {
833 std::env::remove_var("SLACK_RS_TOKENS_PATH");
835
836 let default_path = FileTokenStore::default_path().unwrap();
837 let path_str = default_path.to_string_lossy();
838
839 assert!(
841 path_str.contains(".config/slack-rs/tokens.json")
842 || path_str.contains(".config\\slack-rs\\tokens.json"),
843 "Default path should be ~/.config/slack-rs/tokens.json, got: {}",
844 path_str
845 );
846 }
847
848 #[test]
856 #[serial_test::serial]
857 fn test_unified_credential_storage_policy() {
858 use tempfile::TempDir;
859
860 std::env::remove_var("SLACKRS_TOKEN_STORE");
862 let backend = resolve_token_store_backend().unwrap();
863 assert_eq!(
864 backend,
865 TokenStoreBackend::Keyring,
866 "Default backend should be Keyring"
867 );
868
869 std::env::set_var("SLACKRS_TOKEN_STORE", "file");
871 let backend = resolve_token_store_backend().unwrap();
872 assert_eq!(
873 backend,
874 TokenStoreBackend::File,
875 "Should be able to select File backend"
876 );
877
878 let temp_dir = TempDir::new().unwrap();
880 let tokens_path = temp_dir.path().join("tokens.json");
881 std::env::set_var("SLACK_RS_TOKENS_PATH", tokens_path.to_str().unwrap());
882
883 let memory_store = InMemoryTokenStore::new();
885
886 let file_store = FileTokenStore::with_path(tokens_path.clone()).unwrap();
888
889 let token_key = make_token_key("T123", "U456");
891 let secret_key = make_oauth_client_secret_key("default");
892
893 memory_store.set(&token_key, "token1").unwrap();
895 memory_store.set(&secret_key, "secret1").unwrap();
896
897 file_store.set(&token_key, "token2").unwrap();
899 file_store.set(&secret_key, "secret2").unwrap();
900
901 assert_eq!(memory_store.get(&token_key).unwrap(), "token1");
903 assert_eq!(memory_store.get(&secret_key).unwrap(), "secret1");
904 assert_eq!(file_store.get(&token_key).unwrap(), "token2");
905 assert_eq!(file_store.get(&secret_key).unwrap(), "secret2");
906
907 store_oauth_client_secret(&memory_store, "test", "secret123").unwrap();
909 assert_eq!(
910 get_oauth_client_secret(&memory_store, "test").unwrap(),
911 "secret123"
912 );
913
914 store_oauth_client_secret(&file_store, "test", "secret456").unwrap();
915 assert_eq!(
916 get_oauth_client_secret(&file_store, "test").unwrap(),
917 "secret456"
918 );
919
920 std::env::remove_var("SLACKRS_TOKEN_STORE");
922 std::env::remove_var("SLACK_RS_TOKENS_PATH");
923 }
924
925 #[test]
928 fn test_in_memory_token_store_as_mock() {
929 let store = InMemoryTokenStore::new();
930
931 let token_key = make_token_key("T999", "U888");
933 store.set(&token_key, "xoxb-mock-token").unwrap();
934 assert_eq!(store.get(&token_key).unwrap(), "xoxb-mock-token");
935
936 let secret_key = make_oauth_client_secret_key("mock-profile");
938 store.set(&secret_key, "mock-secret").unwrap();
939 assert_eq!(store.get(&secret_key).unwrap(), "mock-secret");
940
941 assert!(store.exists(&token_key));
943 assert!(store.exists(&secret_key));
944 assert!(!store.exists("nonexistent"));
945
946 store.delete(&token_key).unwrap();
948 assert!(!store.exists(&token_key));
949
950 store_oauth_client_secret(&store, "test", "test-secret").unwrap();
952 assert_eq!(
953 get_oauth_client_secret(&store, "test").unwrap(),
954 "test-secret"
955 );
956 delete_oauth_client_secret(&store, "test").unwrap();
957 assert!(!store.exists(&make_oauth_client_secret_key("test")));
958 }
959}