aptu_core/
auth.rs

1// SPDX-License-Identifier: Apache-2.0
2
3//! Token provider abstraction for multi-platform credential resolution.
4//!
5//! This module defines the `TokenProvider` trait, which abstracts credential
6//! resolution across different platforms (CLI, iOS, etc.). Each platform
7//! implements this trait to provide GitHub and AI provider tokens from their
8//! respective credential sources.
9
10use secrecy::SecretString;
11
12/// Provides GitHub and AI provider credentials for API calls.
13///
14/// This trait abstracts credential resolution across platforms:
15/// - **CLI:** Resolves from environment variables, GitHub CLI, or system keyring
16/// - **iOS:** Resolves from iOS keychain via FFI
17///
18/// Implementations should handle credential lookup and return `None` if
19/// credentials are not available.
20pub trait TokenProvider: Send + Sync {
21    /// Retrieves the GitHub API token.
22    ///
23    /// Returns `None` if no token is available from any source.
24    fn github_token(&self) -> Option<SecretString>;
25
26    /// Retrieves an AI provider API key.
27    ///
28    /// # Arguments
29    /// * `provider` - The AI provider name (must match a registered provider in the registry)
30    ///
31    /// Returns `None` if no API key is available from any source.
32    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 std::collections::HashMap;
40
41    /// Mock implementation for testing.
42    struct MockTokenProvider {
43        github_token: Option<SecretString>,
44        ai_keys: HashMap<String, SecretString>,
45    }
46
47    impl TokenProvider for MockTokenProvider {
48        fn github_token(&self) -> Option<SecretString> {
49            self.github_token.clone()
50        }
51
52        fn ai_api_key(&self, provider: &str) -> Option<SecretString> {
53            self.ai_keys.get(provider).cloned()
54        }
55    }
56
57    #[test]
58    fn test_mock_provider_with_tokens() {
59        let mut ai_keys = HashMap::new();
60        for provider_config in all_providers() {
61            ai_keys.insert(
62                provider_config.name.to_string(),
63                SecretString::from(format!("{}_key", provider_config.name)),
64            );
65        }
66
67        let provider = MockTokenProvider {
68            github_token: Some(SecretString::from("gh_token")),
69            ai_keys,
70        };
71
72        assert!(provider.github_token().is_some());
73        for provider_config in all_providers() {
74            assert!(
75                provider.ai_api_key(provider_config.name).is_some(),
76                "Expected key for provider: {}",
77                provider_config.name
78            );
79        }
80    }
81
82    #[test]
83    fn test_mock_provider_without_tokens() {
84        let provider = MockTokenProvider {
85            github_token: None,
86            ai_keys: HashMap::new(),
87        };
88
89        assert!(provider.github_token().is_none());
90        for provider_config in all_providers() {
91            assert!(
92                provider.ai_api_key(provider_config.name).is_none(),
93                "Expected no key for provider: {}",
94                provider_config.name
95            );
96        }
97    }
98}