Files
certifai/src/infrastructure/config.rs
Sharang Parnerkar 01b285ba20
All checks were successful
CI / Format (push) Successful in 3s
CI / Clippy (push) Successful in 2m56s
CI / Security Audit (push) Has been skipped
CI / Tests (push) Has been skipped
CI / Deploy (push) Has been skipped
CI / Format (pull_request) Successful in 22s
CI / Clippy (pull_request) Successful in 2m51s
CI / Security Audit (pull_request) Has been skipped
CI / Tests (pull_request) Has been skipped
CI / Deploy (pull_request) Has been skipped
test: add comprehensive unit test suite (~85 new tests)
Add unit tests across all model and server infrastructure layers,
increasing test count from 7 to 92. Covers serde round-trips, enum
methods, defaults, config parsing, error mapping, PKCE crypto (with
RFC 7636 test vector), OAuth store, and SearXNG ranking/dedup logic.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-24 18:43:27 +01:00

411 lines
14 KiB
Rust

//! Configuration structs loaded once at startup from environment variables.
//!
//! Each struct provides a `from_env()` constructor that reads `std::env::var`
//! values. Required variables cause an `Error::ConfigError` on failure;
//! optional ones default to an empty string.
use secrecy::SecretString;
use super::Error;
// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------
/// Read a required environment variable or return `Error::ConfigError`.
fn required_env(name: &str) -> Result<String, Error> {
std::env::var(name).map_err(|_| Error::ConfigError(format!("{name} is required but not set")))
}
/// Read an optional environment variable, defaulting to an empty string.
fn optional_env(name: &str) -> String {
std::env::var(name).unwrap_or_default()
}
// ---------------------------------------------------------------------------
// KeycloakConfig
// ---------------------------------------------------------------------------
/// Keycloak OpenID Connect settings for the public (frontend) client.
///
/// Also carries the admin service-account credentials used for
/// server-to-server calls (e.g. user management APIs).
#[derive(Debug)]
pub struct KeycloakConfig {
/// Base URL of the Keycloak instance (e.g. `http://localhost:8080`).
pub url: String,
/// Keycloak realm name.
pub realm: String,
/// Public client ID used by the dashboard frontend.
pub client_id: String,
/// OAuth redirect URI registered in Keycloak.
pub redirect_uri: String,
/// Root URL of this application (used for post-logout redirect).
pub app_url: String,
/// Confidential client ID for admin/server-to-server calls.
pub admin_client_id: String,
/// Confidential client secret (wrapped for debug safety).
pub admin_client_secret: SecretString,
}
impl KeycloakConfig {
/// Load Keycloak configuration from environment variables.
///
/// # Errors
///
/// Returns `Error::ConfigError` if a required variable is missing.
pub fn from_env() -> Result<Self, Error> {
Ok(Self {
url: required_env("KEYCLOAK_URL")?,
realm: required_env("KEYCLOAK_REALM")?,
client_id: required_env("KEYCLOAK_CLIENT_ID")?,
redirect_uri: required_env("REDIRECT_URI")?,
app_url: required_env("APP_URL")?,
admin_client_id: optional_env("KEYCLOAK_ADMIN_CLIENT_ID"),
admin_client_secret: SecretString::from(optional_env("KEYCLOAK_ADMIN_CLIENT_SECRET")),
})
}
/// OpenID Connect authorization endpoint URL.
pub fn auth_endpoint(&self) -> String {
format!(
"{}/realms/{}/protocol/openid-connect/auth",
self.url, self.realm
)
}
/// OpenID Connect token endpoint URL.
pub fn token_endpoint(&self) -> String {
format!(
"{}/realms/{}/protocol/openid-connect/token",
self.url, self.realm
)
}
/// OpenID Connect userinfo endpoint URL.
pub fn userinfo_endpoint(&self) -> String {
format!(
"{}/realms/{}/protocol/openid-connect/userinfo",
self.url, self.realm
)
}
/// OpenID Connect end-session (logout) endpoint URL.
pub fn logout_endpoint(&self) -> String {
format!(
"{}/realms/{}/protocol/openid-connect/logout",
self.url, self.realm
)
}
}
// ---------------------------------------------------------------------------
// SmtpConfig
// ---------------------------------------------------------------------------
/// SMTP mail settings for transactional emails (invites, alerts, etc.).
#[derive(Debug)]
pub struct SmtpConfig {
/// SMTP server hostname.
pub host: String,
/// SMTP server port (as string for flexibility, e.g. "587").
pub port: String,
/// SMTP username.
pub username: String,
/// SMTP password (wrapped for debug safety).
pub password: SecretString,
/// Sender address shown in the `From:` header.
pub from_address: String,
}
impl SmtpConfig {
/// Load SMTP configuration from environment variables.
///
/// All fields are optional; defaults to empty strings when absent.
///
/// # Errors
///
/// Currently infallible but returns `Result` for consistency.
pub fn from_env() -> Result<Self, Error> {
Ok(Self {
host: optional_env("SMTP_HOST"),
port: optional_env("SMTP_PORT"),
username: optional_env("SMTP_USERNAME"),
password: SecretString::from(optional_env("SMTP_PASSWORD")),
from_address: optional_env("SMTP_FROM_ADDRESS"),
})
}
}
// ---------------------------------------------------------------------------
// ServiceUrls
// ---------------------------------------------------------------------------
/// URLs and credentials for external services (Ollama, SearXNG, S3, etc.).
#[derive(Debug)]
pub struct ServiceUrls {
/// Ollama LLM instance base URL.
pub ollama_url: String,
/// Default Ollama model to use.
pub ollama_model: String,
/// SearXNG meta-search engine base URL.
pub searxng_url: String,
/// LangChain service URL.
pub langchain_url: String,
/// LangGraph service URL.
pub langgraph_url: String,
/// Langfuse observability URL.
pub langfuse_url: String,
/// Vector database URL.
pub vectordb_url: String,
/// S3-compatible object storage URL.
pub s3_url: String,
/// S3 access key.
pub s3_access_key: String,
/// S3 secret key (wrapped for debug safety).
pub s3_secret_key: SecretString,
}
impl ServiceUrls {
/// Load service URLs from environment variables.
///
/// All fields are optional with sensible defaults where applicable.
///
/// # Errors
///
/// Currently infallible but returns `Result` for consistency.
pub fn from_env() -> Result<Self, Error> {
Ok(Self {
ollama_url: std::env::var("OLLAMA_URL")
.unwrap_or_else(|_| "http://localhost:11434".into()),
ollama_model: std::env::var("OLLAMA_MODEL").unwrap_or_else(|_| "llama3.1:8b".into()),
searxng_url: std::env::var("SEARXNG_URL")
.unwrap_or_else(|_| "http://localhost:8888".into()),
langchain_url: optional_env("LANGCHAIN_URL"),
langgraph_url: optional_env("LANGGRAPH_URL"),
langfuse_url: optional_env("LANGFUSE_URL"),
vectordb_url: optional_env("VECTORDB_URL"),
s3_url: optional_env("S3_URL"),
s3_access_key: optional_env("S3_ACCESS_KEY"),
s3_secret_key: SecretString::from(optional_env("S3_SECRET_KEY")),
})
}
}
// ---------------------------------------------------------------------------
// StripeConfig
// ---------------------------------------------------------------------------
/// Stripe billing configuration.
#[derive(Debug)]
pub struct StripeConfig {
/// Stripe secret API key (wrapped for debug safety).
pub secret_key: SecretString,
/// Stripe webhook signing secret (wrapped for debug safety).
pub webhook_secret: SecretString,
/// Stripe publishable key (safe to expose to the frontend).
pub publishable_key: String,
}
impl StripeConfig {
/// Load Stripe configuration from environment variables.
///
/// # Errors
///
/// Currently infallible but returns `Result` for consistency.
pub fn from_env() -> Result<Self, Error> {
Ok(Self {
secret_key: SecretString::from(optional_env("STRIPE_SECRET_KEY")),
webhook_secret: SecretString::from(optional_env("STRIPE_WEBHOOK_SECRET")),
publishable_key: optional_env("STRIPE_PUBLISHABLE_KEY"),
})
}
}
// ---------------------------------------------------------------------------
// LlmProvidersConfig
// ---------------------------------------------------------------------------
/// Comma-separated list of enabled LLM provider identifiers.
///
/// For example: `LLM_PROVIDERS=ollama,openai,anthropic`
#[derive(Debug)]
pub struct LlmProvidersConfig {
/// Parsed provider names.
pub providers: Vec<String>,
}
impl LlmProvidersConfig {
/// Load the provider list from `LLM_PROVIDERS`.
///
/// # Errors
///
/// Currently infallible but returns `Result` for consistency.
pub fn from_env() -> Result<Self, Error> {
let raw = optional_env("LLM_PROVIDERS");
let providers: Vec<String> = raw
.split(',')
.map(|s| s.trim().to_string())
.filter(|s| !s.is_empty())
.collect();
Ok(Self { providers })
}
}
#[cfg(test)]
mod tests {
#![allow(clippy::unwrap_used, clippy::expect_used)]
use super::*;
use pretty_assertions::assert_eq;
use serial_test::serial;
// -----------------------------------------------------------------------
// KeycloakConfig endpoint methods (no env vars needed)
// -----------------------------------------------------------------------
fn sample_keycloak() -> KeycloakConfig {
KeycloakConfig {
url: "https://auth.example.com".into(),
realm: "myrealm".into(),
client_id: "dashboard".into(),
redirect_uri: "https://app.example.com/callback".into(),
app_url: "https://app.example.com".into(),
admin_client_id: String::new(),
admin_client_secret: SecretString::from(String::new()),
}
}
#[test]
fn keycloak_auth_endpoint() {
let kc = sample_keycloak();
assert_eq!(
kc.auth_endpoint(),
"https://auth.example.com/realms/myrealm/protocol/openid-connect/auth"
);
}
#[test]
fn keycloak_token_endpoint() {
let kc = sample_keycloak();
assert_eq!(
kc.token_endpoint(),
"https://auth.example.com/realms/myrealm/protocol/openid-connect/token"
);
}
#[test]
fn keycloak_userinfo_endpoint() {
let kc = sample_keycloak();
assert_eq!(
kc.userinfo_endpoint(),
"https://auth.example.com/realms/myrealm/protocol/openid-connect/userinfo"
);
}
#[test]
fn keycloak_logout_endpoint() {
let kc = sample_keycloak();
assert_eq!(
kc.logout_endpoint(),
"https://auth.example.com/realms/myrealm/protocol/openid-connect/logout"
);
}
// -----------------------------------------------------------------------
// LlmProvidersConfig::from_env()
// -----------------------------------------------------------------------
#[test]
#[serial]
fn llm_providers_empty_string() {
std::env::set_var("LLM_PROVIDERS", "");
let cfg = LlmProvidersConfig::from_env().unwrap();
assert!(cfg.providers.is_empty());
std::env::remove_var("LLM_PROVIDERS");
}
#[test]
#[serial]
fn llm_providers_single() {
std::env::set_var("LLM_PROVIDERS", "ollama");
let cfg = LlmProvidersConfig::from_env().unwrap();
assert_eq!(cfg.providers, vec!["ollama"]);
std::env::remove_var("LLM_PROVIDERS");
}
#[test]
#[serial]
fn llm_providers_multiple() {
std::env::set_var("LLM_PROVIDERS", "ollama,openai,anthropic");
let cfg = LlmProvidersConfig::from_env().unwrap();
assert_eq!(cfg.providers, vec!["ollama", "openai", "anthropic"]);
std::env::remove_var("LLM_PROVIDERS");
}
#[test]
#[serial]
fn llm_providers_trims_whitespace() {
std::env::set_var("LLM_PROVIDERS", " ollama , openai ");
let cfg = LlmProvidersConfig::from_env().unwrap();
assert_eq!(cfg.providers, vec!["ollama", "openai"]);
std::env::remove_var("LLM_PROVIDERS");
}
#[test]
#[serial]
fn llm_providers_filters_empty_entries() {
std::env::set_var("LLM_PROVIDERS", "ollama,,openai,");
let cfg = LlmProvidersConfig::from_env().unwrap();
assert_eq!(cfg.providers, vec!["ollama", "openai"]);
std::env::remove_var("LLM_PROVIDERS");
}
// -----------------------------------------------------------------------
// ServiceUrls::from_env() defaults
// -----------------------------------------------------------------------
#[test]
#[serial]
fn service_urls_default_ollama_url() {
std::env::remove_var("OLLAMA_URL");
let svc = ServiceUrls::from_env().unwrap();
assert_eq!(svc.ollama_url, "http://localhost:11434");
}
#[test]
#[serial]
fn service_urls_default_ollama_model() {
std::env::remove_var("OLLAMA_MODEL");
let svc = ServiceUrls::from_env().unwrap();
assert_eq!(svc.ollama_model, "llama3.1:8b");
}
#[test]
#[serial]
fn service_urls_default_searxng_url() {
std::env::remove_var("SEARXNG_URL");
let svc = ServiceUrls::from_env().unwrap();
assert_eq!(svc.searxng_url, "http://localhost:8888");
}
#[test]
#[serial]
fn service_urls_custom_ollama_url() {
std::env::set_var("OLLAMA_URL", "http://gpu-host:11434");
let svc = ServiceUrls::from_env().unwrap();
assert_eq!(svc.ollama_url, "http://gpu-host:11434");
std::env::remove_var("OLLAMA_URL");
}
#[test]
#[serial]
fn required_env_missing_returns_config_error() {
std::env::remove_var("__TEST_REQUIRED_MISSING__");
let result = required_env("__TEST_REQUIRED_MISSING__");
assert!(result.is_err());
let err_msg = result.unwrap_err().to_string();
assert!(err_msg.contains("__TEST_REQUIRED_MISSING__"));
}
}