Skip to main content

mkt_core/auth/
token.rs

1//! Token resolution logic.
2
3use secrecy::{ExposeSecret, SecretString};
4
5use crate::error::{MktError, Result};
6
7/// Resolves an API token from environment or configuration.
8///
9/// Resolution order:
10/// 1. Environment variable (if `env_var_name` is provided)
11/// 2. Config value (if `config_value` is provided)
12/// 3. Returns an `AuthError`
13///
14/// # Errors
15///
16/// Returns [`MktError::AuthError`] if no token can be resolved from either
17/// the environment variable or the config value.
18pub fn resolve_token(
19    provider: &str,
20    env_var_name: &str,
21    config_value: Option<&str>,
22) -> Result<SecretString> {
23    // 1. Try environment variable
24    if let Ok(token) = std::env::var(env_var_name) {
25        if !token.is_empty() {
26            return Ok(SecretString::from(token));
27        }
28    }
29
30    // 2. Try config value
31    if let Some(token) = config_value {
32        if !token.is_empty() {
33            return Ok(SecretString::from(token.to_owned()));
34        }
35    }
36
37    // 3. No token found
38    Err(MktError::auth_error(
39        provider,
40        &format!("No access token found. Set {env_var_name} or configure it in your profile."),
41    ))
42}
43
44/// Verify that a secret string is non-empty (without exposing it).
45pub fn validate_token(token: &SecretString) -> bool {
46    !token.expose_secret().is_empty()
47}
48
49#[cfg(test)]
50mod tests {
51    use super::*;
52
53    #[test]
54    #[allow(clippy::expect_used)]
55    fn resolve_from_config_value() {
56        // Use a key that definitely does NOT exist
57        let key = "MKT_TEST_TOKEN_DEFINITELY_NOT_SET_98765";
58        let result = resolve_token("test", key, Some("config-token"));
59        let token = result.expect("should resolve");
60        assert_eq!(token.expose_secret(), "config-token");
61    }
62
63    #[test]
64    #[allow(clippy::panic)]
65    fn missing_token_returns_auth_error() {
66        let key = "MKT_TEST_TOKEN_DEFINITELY_NOT_SET_11111";
67        let result = resolve_token("test", key, None);
68        let Err(e) = result else {
69            panic!("expected auth error");
70        };
71        assert!(e.to_string().contains("No access token"));
72    }
73
74    #[test]
75    fn empty_config_value_returns_error() {
76        let key = "MKT_TEST_TOKEN_DEFINITELY_NOT_SET_22222";
77        let result = resolve_token("test", key, Some(""));
78        assert!(result.is_err());
79    }
80
81    #[test]
82    fn env_var_resolution_with_existing_var() {
83        // PATH is set on every supported platform (HOME is not on Windows).
84        let result = resolve_token("test", "PATH", None);
85        assert!(result.is_ok());
86    }
87
88    #[test]
89    fn validate_non_empty_token() {
90        let token = SecretString::from("valid".to_owned());
91        assert!(validate_token(&token));
92    }
93
94    #[test]
95    fn validate_empty_token() {
96        let token = SecretString::from(String::new());
97        assert!(!validate_token(&token));
98    }
99}