pub mod keyring;
mod result;
#[cfg(feature = "nika-daemon")]
mod daemon;
#[cfg(not(feature = "nika-daemon"))]
mod fallback;
pub use keyring::{
mask_api_key, migrate_env_to_keyring, validate_key_format, KeyringError, MigrationReport,
NikaKeyring,
};
pub use result::SecretsLoadResult;
#[cfg(feature = "nika-daemon")]
pub use daemon::{daemon_available, get_secret, has_secret, load_from_daemon_or_fallback};
#[cfg(not(feature = "nika-daemon"))]
pub use fallback::{daemon_available, get_secret, has_secret, load_from_daemon_or_fallback};
pub fn provider_env_var(provider: &str) -> &'static str {
crate::core::provider_to_env_var(provider).unwrap_or("UNKNOWN_API_KEY")
}
#[cfg(test)]
mod tests {
use super::*;
use crate::core::{ProviderCategory, KNOWN_PROVIDERS};
use secrecy::ExposeSecret;
use serial_test::serial;
#[test]
fn test_provider_env_var_lookup() {
assert_eq!(provider_env_var("anthropic"), "ANTHROPIC_API_KEY");
assert_eq!(provider_env_var("openai"), "OPENAI_API_KEY");
assert_eq!(provider_env_var("native"), "NIKA_NATIVE_MODEL_PATH");
assert_eq!(provider_env_var("neo4j"), "NEO4J_PASSWORD");
assert_eq!(provider_env_var("github"), "GITHUB_TOKEN");
}
#[test]
fn test_provider_env_var_all_llm_providers() {
assert_eq!(provider_env_var("mistral"), "MISTRAL_API_KEY");
assert_eq!(provider_env_var("groq"), "GROQ_API_KEY");
assert_eq!(provider_env_var("deepseek"), "DEEPSEEK_API_KEY");
assert_eq!(provider_env_var("gemini"), "GEMINI_API_KEY");
}
#[test]
fn test_provider_env_var_unknown() {
assert_eq!(provider_env_var("nonexistent"), "UNKNOWN_API_KEY");
}
#[test]
fn test_provider_env_var_empty_string() {
assert_eq!(provider_env_var(""), "UNKNOWN_API_KEY");
}
#[test]
fn test_daemon_available_check() {
#[cfg(not(feature = "nika-daemon"))]
assert!(!daemon_available());
#[cfg(feature = "nika-daemon")]
{
let result = daemon_available();
let _ = result;
}
}
#[tokio::test]
#[serial]
async fn test_get_secret_returns_env_var_value() {
let key = "ANTHROPIC_API_KEY";
let original = std::env::var(key).ok();
std::env::set_var(key, "sk-ant-test-key-12345");
let secret = get_secret("anthropic").await;
assert!(secret.is_some());
assert_eq!(secret.unwrap().expose_secret(), "sk-ant-test-key-12345");
match original {
Some(v) => std::env::set_var(key, v),
None => unsafe { std::env::remove_var(key) },
}
}
#[test]
fn test_empty_env_var_is_not_a_secret() {
let key = "DEEPSEEK_API_KEY";
let original = std::env::var(key).ok();
std::env::set_var(key, "");
let value = std::env::var(key).ok().filter(|v| !v.is_empty());
assert!(value.is_none(), "empty env var should be filtered out");
match original {
Some(v) => std::env::set_var(key, v),
None => unsafe { std::env::remove_var(key) },
}
}
#[tokio::test]
#[serial]
async fn test_get_secret_returns_none_when_env_unset() {
let key = "GROQ_API_KEY";
let original = std::env::var(key).ok();
unsafe { std::env::remove_var(key) };
let secret = get_secret("groq").await;
assert!(secret.is_none());
if let Some(v) = original {
std::env::set_var(key, v);
}
}
#[tokio::test]
async fn test_get_secret_unknown_provider_returns_none() {
let secret = get_secret("nonexistent_provider").await;
assert!(secret.is_none());
}
#[tokio::test]
#[serial]
async fn test_has_secret_true_when_env_set() {
let key = "MISTRAL_API_KEY";
let original = std::env::var(key).ok();
std::env::set_var(key, "test-key");
assert!(has_secret("mistral").await);
match original {
Some(v) => std::env::set_var(key, v),
None => unsafe { std::env::remove_var(key) },
}
}
#[tokio::test]
#[serial]
async fn test_has_secret_false_when_env_unset() {
let key = "GEMINI_API_KEY";
let original = std::env::var(key).ok();
unsafe { std::env::remove_var(key) };
assert!(!has_secret("gemini").await);
if let Some(v) = original {
std::env::set_var(key, v);
}
}
#[tokio::test]
async fn test_has_secret_unknown_provider_false() {
assert!(!has_secret("fantasy_provider").await);
}
#[tokio::test]
#[serial]
async fn test_has_secret_false_when_env_empty() {
let key = "XAI_API_KEY";
let original = std::env::var(key).ok();
std::env::set_var(key, "");
assert!(
!has_secret("xai").await,
"has_secret() must return false for empty env var"
);
match original {
Some(v) => std::env::set_var(key, v),
None => unsafe { std::env::remove_var(key) },
}
}
#[tokio::test]
#[serial]
async fn test_get_secret_returns_none_when_env_empty() {
let key = "GROQ_API_KEY";
let original = std::env::var(key).ok();
std::env::set_var(key, "");
let secret = get_secret("groq").await;
assert!(
secret.is_none(),
"get_secret() must return None for empty env var"
);
match original {
Some(v) => std::env::set_var(key, v),
None => unsafe { std::env::remove_var(key) },
}
}
#[tokio::test]
#[serial]
async fn test_load_does_not_count_empty_env_var_as_found() {
let key = "DEEPSEEK_API_KEY";
let original = std::env::var(key).ok();
std::env::set_var(key, "");
let result = load_from_daemon_or_fallback().await;
let found_in_daemon = result.from_daemon.contains(&"deepseek".to_string());
let found_in_fallback = result.from_fallback.contains(&"deepseek".to_string());
assert!(
!found_in_daemon && !found_in_fallback,
"Empty env var should not count as found: daemon={:?}, fallback={:?}",
result.from_daemon,
result.from_fallback
);
match original {
Some(v) => std::env::set_var(key, v),
None => unsafe { std::env::remove_var(key) },
}
}
#[tokio::test]
async fn test_load_result_structure() {
let result = load_from_daemon_or_fallback().await;
let expected_count = if cfg!(feature = "nika-daemon") {
KNOWN_PROVIDERS.len()
} else {
KNOWN_PROVIDERS
.iter()
.filter(|p| p.category == ProviderCategory::Llm)
.count()
};
let total = result.from_daemon.len() + result.from_fallback.len() + result.not_found.len();
assert_eq!(
total,
expected_count,
"Should process exactly {} providers, got {} daemon + {} fallback + {} not_found",
expected_count,
result.from_daemon.len(),
result.from_fallback.len(),
result.not_found.len()
);
}
#[tokio::test]
#[serial]
async fn test_load_result_detects_env_vars() {
let key = "OPENAI_API_KEY";
let original = std::env::var(key).ok();
std::env::set_var(key, "sk-test-openai-key");
let result = load_from_daemon_or_fallback().await;
let found_in_daemon = result.from_daemon.contains(&"openai".to_string());
let found_in_fallback = result.from_fallback.contains(&"openai".to_string());
assert!(
found_in_daemon || found_in_fallback,
"openai should be detected from env, got daemon={:?}, fallback={:?}",
result.from_daemon,
result.from_fallback
);
match original {
Some(v) => std::env::set_var(key, v),
None => unsafe { std::env::remove_var(key) },
}
}
#[tokio::test]
async fn test_load_result_summary_not_empty() {
let result = load_from_daemon_or_fallback().await;
let summary = result.summary();
assert!(!summary.is_empty(), "Summary should not be empty");
assert!(
summary.contains("daemon"),
"Summary should mention daemon status: {}",
summary
);
}
}