1use secrecy::SecretString;
11
12pub trait TokenProvider: Send + Sync {
21 fn github_token(&self) -> Option<SecretString>;
25
26 fn ai_api_key(&self, provider: &str) -> Option<SecretString>;
33}
34
35#[cfg(test)]
36mod tests {
37 use super::*;
38 use crate::ai::registry::all_providers;
39 use secrecy::ExposeSecret;
40 use serial_test::serial;
41 use std::collections::HashMap;
42
43 struct MockTokenProvider {
45 github_token: Option<SecretString>,
46 ai_keys: HashMap<String, SecretString>,
47 }
48
49 impl Drop for MockTokenProvider {
50 fn drop(&mut self) {
51 use zeroize::Zeroize;
52 if let Some(ref mut gh_token) = self.github_token {
53 gh_token.zeroize();
54 }
55 for ai_key in self.ai_keys.values_mut() {
56 ai_key.zeroize();
57 }
58 }
59 }
60
61 impl TokenProvider for MockTokenProvider {
62 fn github_token(&self) -> Option<SecretString> {
63 self.github_token.clone()
64 }
65
66 fn ai_api_key(&self, provider: &str) -> Option<SecretString> {
67 self.ai_keys.get(provider).cloned()
68 }
69 }
70
71 pub struct EnvTokenProvider;
77
78 impl TokenProvider for EnvTokenProvider {
79 fn github_token(&self) -> Option<SecretString> {
80 std::env::var("GITHUB_TOKEN")
81 .or_else(|_| std::env::var("GH_TOKEN"))
82 .ok()
83 .map(SecretString::from)
84 }
85
86 fn ai_api_key(&self, provider: &str) -> Option<SecretString> {
87 let var = format!("{}_API_KEY", provider.to_uppercase().replace('-', "_"));
88 std::env::var(&var).ok().map(SecretString::from)
89 }
90 }
91
92 #[test]
93 fn test_mock_provider_with_tokens() {
94 let mut ai_keys = HashMap::new();
95 for provider_config in all_providers() {
96 ai_keys.insert(
97 provider_config.name.to_string(),
98 SecretString::from(format!("{}_key", provider_config.name)),
99 );
100 }
101
102 let provider = MockTokenProvider {
103 github_token: Some(SecretString::from("gh_token")),
104 ai_keys,
105 };
106
107 assert!(provider.github_token().is_some());
108 for provider_config in all_providers() {
109 assert!(
110 provider.ai_api_key(provider_config.name).is_some(),
111 "Expected key for provider: {}",
112 provider_config.name
113 );
114 }
115 }
116
117 #[test]
118 fn test_mock_provider_without_tokens() {
119 let provider = MockTokenProvider {
120 github_token: None,
121 ai_keys: HashMap::new(),
122 };
123
124 assert!(provider.github_token().is_none());
125 for provider_config in all_providers() {
126 assert!(
127 provider.ai_api_key(provider_config.name).is_none(),
128 "Expected no key for provider: {}",
129 provider_config.name
130 );
131 }
132 }
133
134 #[test]
135 #[serial]
136 fn test_env_token_provider_github_token() {
137 unsafe {
139 std::env::set_var("GITHUB_TOKEN", "test_gh_token_abc");
140 }
141 let provider = EnvTokenProvider;
142 let result = provider.github_token();
143 unsafe {
144 std::env::remove_var("GITHUB_TOKEN");
145 }
146 assert!(result.is_some());
147 assert_eq!(result.unwrap().expose_secret(), "test_gh_token_abc");
148 }
149
150 #[test]
151 #[serial]
152 fn test_env_token_provider_ai_api_key() {
153 unsafe {
155 std::env::set_var("OPENAI_API_KEY", "sk-test-key");
156 }
157 let provider = EnvTokenProvider;
158 let result = provider.ai_api_key("openai");
159 unsafe {
160 std::env::remove_var("OPENAI_API_KEY");
161 }
162 assert!(result.is_some());
163 assert_eq!(result.unwrap().expose_secret(), "sk-test-key");
164 }
165
166 #[test]
167 #[serial]
168 fn test_env_token_provider_no_env() {
169 unsafe {
171 std::env::remove_var("GITHUB_TOKEN");
172 std::env::remove_var("GH_TOKEN");
173 }
174 let provider = EnvTokenProvider;
175 assert!(provider.github_token().is_none());
176 }
177}