Skip to main content

vibesql_server/
config.rs

1use anyhow::Result;
2use serde::{Deserialize, Serialize};
3use std::env;
4use std::fs;
5use std::path::PathBuf;
6
7use crate::observability::ObservabilityConfig;
8use crate::subscription::SubscriptionConfig;
9
10/// Server configuration
11#[derive(Debug, Clone, Serialize, Deserialize)]
12pub struct Config {
13    pub server: ServerConfig,
14    pub auth: AuthConfig,
15    pub logging: LoggingConfig,
16    #[serde(default)]
17    pub http: HttpConfig,
18    #[serde(default)]
19    pub observability: ObservabilityConfig,
20    #[serde(default)]
21    pub subscriptions: SubscriptionConfig,
22}
23
24#[derive(Debug, Clone, Serialize, Deserialize)]
25pub struct ServerConfig {
26    /// Host to bind to (default: 0.0.0.0)
27    pub host: String,
28    /// Port to listen on (default: 5432)
29    pub port: u16,
30    /// Maximum concurrent connections (default: 100)
31    pub max_connections: usize,
32    /// Enable SSL/TLS (default: false)
33    pub ssl_enabled: bool,
34    /// SSL certificate file path
35    pub ssl_cert: Option<PathBuf>,
36    /// SSL key file path
37    pub ssl_key: Option<PathBuf>,
38}
39
40#[derive(Debug, Clone, Serialize, Deserialize)]
41pub struct AuthConfig {
42    /// Authentication method: trust, password, md5, scram-sha-256
43    pub method: String,
44    /// Password file path (for file-based auth)
45    pub password_file: Option<PathBuf>,
46}
47
48#[derive(Debug, Clone, Serialize, Deserialize)]
49pub struct LoggingConfig {
50    /// Log level: trace, debug, info, warn, error
51    pub level: String,
52    /// Log file path (optional)
53    pub file: Option<PathBuf>,
54}
55
56#[derive(Debug, Clone, Serialize, Deserialize)]
57pub struct HttpConfig {
58    /// Enable HTTP REST API (default: true)
59    pub enabled: bool,
60    /// HTTP server host (default: 0.0.0.0)
61    pub host: String,
62    /// HTTP server port (default: 8080)
63    pub port: u16,
64    /// HTTP authentication configuration
65    #[serde(default)]
66    pub auth: HttpAuthConfig,
67}
68
69impl Default for HttpConfig {
70    fn default() -> Self {
71        Self {
72            enabled: true,
73            host: "0.0.0.0".to_string(),
74            port: 8080,
75            auth: HttpAuthConfig::default(),
76        }
77    }
78}
79
80/// HTTP API authentication configuration
81#[derive(Debug, Clone, Serialize, Deserialize)]
82pub struct HttpAuthConfig {
83    /// Enable authentication for HTTP API (default: false for backward compatibility)
84    pub enabled: bool,
85    /// Allowed authentication methods: api_key, basic, jwt
86    pub methods: Vec<HttpAuthMethod>,
87    /// API key configuration
88    #[serde(default)]
89    pub api_keys: ApiKeyConfig,
90    /// JWT configuration
91    #[serde(default)]
92    pub jwt: JwtConfig,
93}
94
95impl Default for HttpAuthConfig {
96    fn default() -> Self {
97        Self {
98            enabled: false,
99            methods: vec![HttpAuthMethod::ApiKey, HttpAuthMethod::Basic],
100            api_keys: ApiKeyConfig::default(),
101            jwt: JwtConfig::default(),
102        }
103    }
104}
105
106/// Supported HTTP authentication methods
107#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
108#[serde(rename_all = "snake_case")]
109pub enum HttpAuthMethod {
110    /// API key authentication via Bearer token
111    ApiKey,
112    /// Basic HTTP authentication
113    Basic,
114    /// JWT authentication
115    Jwt,
116}
117
118/// API key configuration
119#[derive(Debug, Clone, Default, Serialize, Deserialize)]
120pub struct ApiKeyConfig {
121    /// List of valid API keys
122    #[serde(default)]
123    pub keys: Vec<String>,
124}
125
126/// JWT configuration
127#[derive(Debug, Clone, Serialize, Deserialize)]
128pub struct JwtConfig {
129    /// Secret key for JWT signing/verification (HS256)
130    #[serde(default)]
131    pub secret: String,
132    /// Expected issuer (iss claim)
133    #[serde(default)]
134    pub issuer: Option<String>,
135    /// Expected audience (aud claim)
136    #[serde(default)]
137    pub audience: Option<String>,
138    /// Token expiration time in seconds (default: 3600 = 1 hour)
139    #[serde(default = "default_jwt_expiration")]
140    pub expiration_secs: u64,
141}
142
143fn default_jwt_expiration() -> u64 {
144    3600
145}
146
147impl Default for JwtConfig {
148    fn default() -> Self {
149        Self {
150            secret: String::new(),
151            issuer: Some("vibesql".to_string()),
152            audience: Some("vibesql-api".to_string()),
153            expiration_secs: default_jwt_expiration(),
154        }
155    }
156}
157
158impl Default for Config {
159    fn default() -> Self {
160        Self {
161            server: ServerConfig {
162                host: "0.0.0.0".to_string(),
163                port: 5432,
164                max_connections: 100,
165                ssl_enabled: false,
166                ssl_cert: None,
167                ssl_key: None,
168            },
169            auth: AuthConfig { method: "trust".to_string(), password_file: None },
170            logging: LoggingConfig { level: "info".to_string(), file: None },
171            http: HttpConfig::default(),
172            observability: ObservabilityConfig::default(),
173            subscriptions: SubscriptionConfig::default(),
174        }
175    }
176}
177
178impl Config {
179    /// Load configuration from file
180    /// Searches for vibesql-server.toml in:
181    /// 1. Current directory
182    /// 2. $HOME/.config/vibesql/
183    /// 3. /etc/vibesql/
184    pub fn load() -> Result<Self> {
185        let config_paths = vec![
186            PathBuf::from("vibesql-server.toml"),
187            dirs::config_dir()
188                .map(|p| p.join("vibesql").join("vibesql-server.toml"))
189                .unwrap_or_default(),
190            PathBuf::from("/etc/vibesql/vibesql-server.toml"),
191        ];
192
193        for path in config_paths {
194            if path.exists() {
195                let contents = fs::read_to_string(&path)?;
196                let mut config: Config = toml::from_str(&contents)?;
197                config.apply_env_overrides();
198                return Ok(config);
199            }
200        }
201
202        // No config file found, return error
203        Err(anyhow::anyhow!("No configuration file found"))
204    }
205
206    /// Load configuration from specific file
207    #[allow(dead_code)]
208    pub fn load_from(path: &PathBuf) -> Result<Self> {
209        let contents = fs::read_to_string(path)?;
210        let mut config: Config = toml::from_str(&contents)?;
211        config.apply_env_overrides();
212        Ok(config)
213    }
214
215    /// Apply environment variable overrides to the configuration.
216    ///
217    /// Environment variables with the `VIBESQL_` prefix override configuration file values.
218    /// This follows the precedence: environment variable > config file > default.
219    ///
220    /// # Supported Environment Variables
221    ///
222    /// | Environment Variable | Config Path |
223    /// |---------------------|-------------|
224    /// | `VIBESQL_SERVER_HOST` | `server.host` |
225    /// | `VIBESQL_SERVER_PORT` | `server.port` |
226    /// | `VIBESQL_SERVER_MAX_CONNECTIONS` | `server.max_connections` |
227    /// | `VIBESQL_SERVER_SSL_ENABLED` | `server.ssl_enabled` |
228    /// | `VIBESQL_SERVER_SSL_CERT` | `server.ssl_cert` |
229    /// | `VIBESQL_SERVER_SSL_KEY` | `server.ssl_key` |
230    /// | `VIBESQL_AUTH_METHOD` | `auth.method` |
231    /// | `VIBESQL_AUTH_PASSWORD_FILE` | `auth.password_file` |
232    /// | `VIBESQL_LOG_LEVEL` | `logging.level` |
233    /// | `VIBESQL_LOG_FILE` | `logging.file` |
234    /// | `VIBESQL_HTTP_ENABLED` | `http.enabled` |
235    /// | `VIBESQL_HTTP_HOST` | `http.host` |
236    /// | `VIBESQL_HTTP_PORT` | `http.port` |
237    /// | `VIBESQL_HTTP_AUTH_ENABLED` | `http.auth.enabled` |
238    /// | `VIBESQL_HTTP_AUTH_METHODS` | `http.auth.methods` |
239    /// | `VIBESQL_HTTP_AUTH_API_KEYS` | `http.auth.api_keys.keys` |
240    /// | `VIBESQL_HTTP_AUTH_JWT_SECRET` | `http.auth.jwt.secret` |
241    /// | `VIBESQL_HTTP_AUTH_JWT_ISSUER` | `http.auth.jwt.issuer` |
242    /// | `VIBESQL_HTTP_AUTH_JWT_AUDIENCE` | `http.auth.jwt.audience` |
243    /// | `VIBESQL_HTTP_AUTH_JWT_EXPIRATION` | `http.auth.jwt.expiration_secs` |
244    pub fn apply_env_overrides(&mut self) {
245        // Server configuration
246        if let Ok(val) = env::var("VIBESQL_SERVER_HOST") {
247            self.server.host = val;
248        }
249        if let Ok(val) = env::var("VIBESQL_SERVER_PORT") {
250            if let Ok(port) = val.parse() {
251                self.server.port = port;
252            }
253        }
254        if let Ok(val) = env::var("VIBESQL_SERVER_MAX_CONNECTIONS") {
255            if let Ok(max_conn) = val.parse() {
256                self.server.max_connections = max_conn;
257            }
258        }
259        if let Ok(val) = env::var("VIBESQL_SERVER_SSL_ENABLED") {
260            self.server.ssl_enabled = parse_bool(&val);
261        }
262        if let Ok(val) = env::var("VIBESQL_SERVER_SSL_CERT") {
263            self.server.ssl_cert = Some(PathBuf::from(val));
264        }
265        if let Ok(val) = env::var("VIBESQL_SERVER_SSL_KEY") {
266            self.server.ssl_key = Some(PathBuf::from(val));
267        }
268
269        // Auth configuration
270        if let Ok(val) = env::var("VIBESQL_AUTH_METHOD") {
271            self.auth.method = val;
272        }
273        if let Ok(val) = env::var("VIBESQL_AUTH_PASSWORD_FILE") {
274            self.auth.password_file = Some(PathBuf::from(val));
275        }
276
277        // Logging configuration
278        if let Ok(val) = env::var("VIBESQL_LOG_LEVEL") {
279            self.logging.level = val;
280        }
281        if let Ok(val) = env::var("VIBESQL_LOG_FILE") {
282            self.logging.file = Some(PathBuf::from(val));
283        }
284
285        // HTTP configuration
286        if let Ok(val) = env::var("VIBESQL_HTTP_ENABLED") {
287            self.http.enabled = parse_bool(&val);
288        }
289        if let Ok(val) = env::var("VIBESQL_HTTP_HOST") {
290            self.http.host = val;
291        }
292        if let Ok(val) = env::var("VIBESQL_HTTP_PORT") {
293            if let Ok(port) = val.parse() {
294                self.http.port = port;
295            }
296        }
297
298        // HTTP Auth configuration
299        if let Ok(val) = env::var("VIBESQL_HTTP_AUTH_ENABLED") {
300            self.http.auth.enabled = parse_bool(&val);
301        }
302        if let Ok(val) = env::var("VIBESQL_HTTP_AUTH_METHODS") {
303            let methods: Vec<HttpAuthMethod> = val
304                .split(',')
305                .filter_map(|s| match s.trim().to_lowercase().as_str() {
306                    "api_key" => Some(HttpAuthMethod::ApiKey),
307                    "basic" => Some(HttpAuthMethod::Basic),
308                    "jwt" => Some(HttpAuthMethod::Jwt),
309                    _ => None,
310                })
311                .collect();
312            if !methods.is_empty() {
313                self.http.auth.methods = methods;
314            }
315        }
316        if let Ok(val) = env::var("VIBESQL_HTTP_AUTH_API_KEYS") {
317            let keys: Vec<String> = val
318                .split(',')
319                .map(|s| s.trim().to_string())
320                .filter(|s| !s.is_empty())
321                .collect();
322            if !keys.is_empty() {
323                self.http.auth.api_keys.keys = keys;
324            }
325        }
326
327        // JWT configuration
328        if let Ok(val) = env::var("VIBESQL_HTTP_AUTH_JWT_SECRET") {
329            self.http.auth.jwt.secret = val;
330        }
331        if let Ok(val) = env::var("VIBESQL_HTTP_AUTH_JWT_ISSUER") {
332            self.http.auth.jwt.issuer = Some(val);
333        }
334        if let Ok(val) = env::var("VIBESQL_HTTP_AUTH_JWT_AUDIENCE") {
335            self.http.auth.jwt.audience = Some(val);
336        }
337        if let Ok(val) = env::var("VIBESQL_HTTP_AUTH_JWT_EXPIRATION") {
338            if let Ok(secs) = val.parse() {
339                self.http.auth.jwt.expiration_secs = secs;
340            }
341        }
342    }
343}
344
345/// Parse a string value as a boolean.
346/// Accepts "true", "1", "yes", "on" as true (case-insensitive).
347/// All other values are considered false.
348fn parse_bool(val: &str) -> bool {
349    matches!(val.to_lowercase().as_str(), "true" | "1" | "yes" | "on")
350}
351
352#[cfg(test)]
353mod tests {
354    use super::*;
355    use std::sync::Mutex;
356
357    // Environment variable tests must be serialized to avoid interference
358    static ENV_TEST_MUTEX: Mutex<()> = Mutex::new(());
359
360    #[test]
361    fn test_default_config() {
362        let config = Config::default();
363        assert_eq!(config.server.host, "0.0.0.0");
364        assert_eq!(config.server.port, 5432);
365        assert_eq!(config.server.max_connections, 100);
366        assert!(!config.server.ssl_enabled);
367        assert_eq!(config.auth.method, "trust");
368    }
369
370    #[test]
371    fn test_config_serialization() {
372        let config = Config::default();
373        let toml_str = toml::to_string(&config).unwrap();
374        let deserialized: Config = toml::from_str(&toml_str).unwrap();
375        assert_eq!(config.server.port, deserialized.server.port);
376    }
377
378    #[test]
379    fn test_selective_updates_config_defaults() {
380        let config = Config::default();
381        assert!(config.subscriptions.selective_updates.enabled);
382        assert_eq!(config.subscriptions.selective_updates.min_changed_columns, 1);
383        assert!((config.subscriptions.selective_updates.max_changed_columns_ratio - 0.5).abs() < 0.001);
384    }
385
386    #[test]
387    fn test_selective_updates_config_from_toml() {
388        let toml_str = r#"
389[server]
390host = "0.0.0.0"
391port = 5432
392max_connections = 100
393ssl_enabled = false
394
395[auth]
396method = "trust"
397
398[logging]
399level = "info"
400
401[subscriptions.selective_updates]
402enabled = false
403min_changed_columns = 2
404max_changed_columns_ratio = 0.75
405"#;
406        let config: Config = toml::from_str(toml_str).unwrap();
407        assert!(!config.subscriptions.selective_updates.enabled);
408        assert_eq!(config.subscriptions.selective_updates.min_changed_columns, 2);
409        assert!((config.subscriptions.selective_updates.max_changed_columns_ratio - 0.75).abs() < 0.001);
410    }
411
412    #[test]
413    fn test_selective_updates_config_partial_override() {
414        // Test that partial config falls back to defaults for unspecified fields
415        let toml_str = r#"
416[server]
417host = "0.0.0.0"
418port = 5432
419max_connections = 100
420ssl_enabled = false
421
422[auth]
423method = "trust"
424
425[logging]
426level = "info"
427
428[subscriptions.selective_updates]
429enabled = false
430"#;
431        let config: Config = toml::from_str(toml_str).unwrap();
432        assert!(!config.subscriptions.selective_updates.enabled);
433        // Other fields should use defaults
434        assert_eq!(config.subscriptions.selective_updates.min_changed_columns, 1);
435        assert!((config.subscriptions.selective_updates.max_changed_columns_ratio - 0.5).abs() < 0.001);
436    }
437
438    #[test]
439    fn test_selective_updates_config_serialization() {
440        let config = Config::default();
441        let toml_str = toml::to_string(&config).unwrap();
442        let deserialized: Config = toml::from_str(&toml_str).unwrap();
443
444        assert_eq!(
445            config.subscriptions.selective_updates.enabled,
446            deserialized.subscriptions.selective_updates.enabled
447        );
448        assert_eq!(
449            config.subscriptions.selective_updates.min_changed_columns,
450            deserialized.subscriptions.selective_updates.min_changed_columns
451        );
452        assert!(
453            (config.subscriptions.selective_updates.max_changed_columns_ratio
454                - deserialized.subscriptions.selective_updates.max_changed_columns_ratio).abs() < 0.001
455        );
456    }
457
458    #[test]
459    fn test_parse_bool() {
460        assert!(parse_bool("true"));
461        assert!(parse_bool("TRUE"));
462        assert!(parse_bool("True"));
463        assert!(parse_bool("1"));
464        assert!(parse_bool("yes"));
465        assert!(parse_bool("YES"));
466        assert!(parse_bool("on"));
467        assert!(parse_bool("ON"));
468
469        assert!(!parse_bool("false"));
470        assert!(!parse_bool("0"));
471        assert!(!parse_bool("no"));
472        assert!(!parse_bool("off"));
473        assert!(!parse_bool(""));
474        assert!(!parse_bool("invalid"));
475    }
476
477    #[test]
478    fn test_env_override_server_host() {
479        let _lock = ENV_TEST_MUTEX.lock().unwrap();
480        let mut config = Config::default();
481        assert_eq!(config.server.host, "0.0.0.0");
482
483        env::set_var("VIBESQL_SERVER_HOST", "127.0.0.1");
484        config.apply_env_overrides();
485        assert_eq!(config.server.host, "127.0.0.1");
486        env::remove_var("VIBESQL_SERVER_HOST");
487    }
488
489    #[test]
490    fn test_env_override_server_port() {
491        let _lock = ENV_TEST_MUTEX.lock().unwrap();
492        let mut config = Config::default();
493        assert_eq!(config.server.port, 5432);
494
495        env::set_var("VIBESQL_SERVER_PORT", "5433");
496        config.apply_env_overrides();
497        assert_eq!(config.server.port, 5433);
498        env::remove_var("VIBESQL_SERVER_PORT");
499    }
500
501    #[test]
502    fn test_env_override_server_port_invalid() {
503        let _lock = ENV_TEST_MUTEX.lock().unwrap();
504        let mut config = Config::default();
505        assert_eq!(config.server.port, 5432);
506
507        // Invalid port should be ignored
508        env::set_var("VIBESQL_SERVER_PORT", "not_a_number");
509        config.apply_env_overrides();
510        assert_eq!(config.server.port, 5432);
511        env::remove_var("VIBESQL_SERVER_PORT");
512    }
513
514    #[test]
515    fn test_env_override_max_connections() {
516        let _lock = ENV_TEST_MUTEX.lock().unwrap();
517        let mut config = Config::default();
518        assert_eq!(config.server.max_connections, 100);
519
520        env::set_var("VIBESQL_SERVER_MAX_CONNECTIONS", "500");
521        config.apply_env_overrides();
522        assert_eq!(config.server.max_connections, 500);
523        env::remove_var("VIBESQL_SERVER_MAX_CONNECTIONS");
524    }
525
526    #[test]
527    fn test_env_override_ssl_enabled() {
528        let _lock = ENV_TEST_MUTEX.lock().unwrap();
529        let mut config = Config::default();
530        assert!(!config.server.ssl_enabled);
531
532        env::set_var("VIBESQL_SERVER_SSL_ENABLED", "true");
533        config.apply_env_overrides();
534        assert!(config.server.ssl_enabled);
535        env::remove_var("VIBESQL_SERVER_SSL_ENABLED");
536    }
537
538    #[test]
539    fn test_env_override_ssl_cert_and_key() {
540        let _lock = ENV_TEST_MUTEX.lock().unwrap();
541        let mut config = Config::default();
542        assert!(config.server.ssl_cert.is_none());
543        assert!(config.server.ssl_key.is_none());
544
545        env::set_var("VIBESQL_SERVER_SSL_CERT", "/path/to/cert.pem");
546        env::set_var("VIBESQL_SERVER_SSL_KEY", "/path/to/key.pem");
547        config.apply_env_overrides();
548        assert_eq!(config.server.ssl_cert, Some(PathBuf::from("/path/to/cert.pem")));
549        assert_eq!(config.server.ssl_key, Some(PathBuf::from("/path/to/key.pem")));
550        env::remove_var("VIBESQL_SERVER_SSL_CERT");
551        env::remove_var("VIBESQL_SERVER_SSL_KEY");
552    }
553
554    #[test]
555    fn test_env_override_auth_method() {
556        let _lock = ENV_TEST_MUTEX.lock().unwrap();
557        let mut config = Config::default();
558        assert_eq!(config.auth.method, "trust");
559
560        env::set_var("VIBESQL_AUTH_METHOD", "scram-sha-256");
561        config.apply_env_overrides();
562        assert_eq!(config.auth.method, "scram-sha-256");
563        env::remove_var("VIBESQL_AUTH_METHOD");
564    }
565
566    #[test]
567    fn test_env_override_auth_password_file() {
568        let _lock = ENV_TEST_MUTEX.lock().unwrap();
569        let mut config = Config::default();
570        assert!(config.auth.password_file.is_none());
571
572        env::set_var("VIBESQL_AUTH_PASSWORD_FILE", "/etc/vibesql/passwords");
573        config.apply_env_overrides();
574        assert_eq!(config.auth.password_file, Some(PathBuf::from("/etc/vibesql/passwords")));
575        env::remove_var("VIBESQL_AUTH_PASSWORD_FILE");
576    }
577
578    #[test]
579    fn test_env_override_log_level() {
580        let _lock = ENV_TEST_MUTEX.lock().unwrap();
581        let mut config = Config::default();
582        assert_eq!(config.logging.level, "info");
583
584        env::set_var("VIBESQL_LOG_LEVEL", "debug");
585        config.apply_env_overrides();
586        assert_eq!(config.logging.level, "debug");
587        env::remove_var("VIBESQL_LOG_LEVEL");
588    }
589
590    #[test]
591    fn test_env_override_log_file() {
592        let _lock = ENV_TEST_MUTEX.lock().unwrap();
593        let mut config = Config::default();
594        assert!(config.logging.file.is_none());
595
596        env::set_var("VIBESQL_LOG_FILE", "/var/log/vibesql/server.log");
597        config.apply_env_overrides();
598        assert_eq!(config.logging.file, Some(PathBuf::from("/var/log/vibesql/server.log")));
599        env::remove_var("VIBESQL_LOG_FILE");
600    }
601
602    #[test]
603    fn test_env_override_http_enabled() {
604        let _lock = ENV_TEST_MUTEX.lock().unwrap();
605        let mut config = Config::default();
606        assert!(config.http.enabled);
607
608        env::set_var("VIBESQL_HTTP_ENABLED", "false");
609        config.apply_env_overrides();
610        assert!(!config.http.enabled);
611        env::remove_var("VIBESQL_HTTP_ENABLED");
612    }
613
614    #[test]
615    fn test_env_override_http_host() {
616        let _lock = ENV_TEST_MUTEX.lock().unwrap();
617        let mut config = Config::default();
618        assert_eq!(config.http.host, "0.0.0.0");
619
620        env::set_var("VIBESQL_HTTP_HOST", "localhost");
621        config.apply_env_overrides();
622        assert_eq!(config.http.host, "localhost");
623        env::remove_var("VIBESQL_HTTP_HOST");
624    }
625
626    #[test]
627    fn test_env_override_http_port() {
628        let _lock = ENV_TEST_MUTEX.lock().unwrap();
629        let mut config = Config::default();
630        assert_eq!(config.http.port, 8080);
631
632        env::set_var("VIBESQL_HTTP_PORT", "9090");
633        config.apply_env_overrides();
634        assert_eq!(config.http.port, 9090);
635        env::remove_var("VIBESQL_HTTP_PORT");
636    }
637
638    #[test]
639    fn test_env_override_multiple_values() {
640        let _lock = ENV_TEST_MUTEX.lock().unwrap();
641        let mut config = Config::default();
642
643        env::set_var("VIBESQL_SERVER_HOST", "192.168.1.1");
644        env::set_var("VIBESQL_SERVER_PORT", "5433");
645        env::set_var("VIBESQL_AUTH_METHOD", "md5");
646        env::set_var("VIBESQL_LOG_LEVEL", "warn");
647        env::set_var("VIBESQL_HTTP_PORT", "8081");
648
649        config.apply_env_overrides();
650
651        assert_eq!(config.server.host, "192.168.1.1");
652        assert_eq!(config.server.port, 5433);
653        assert_eq!(config.auth.method, "md5");
654        assert_eq!(config.logging.level, "warn");
655        assert_eq!(config.http.port, 8081);
656
657        env::remove_var("VIBESQL_SERVER_HOST");
658        env::remove_var("VIBESQL_SERVER_PORT");
659        env::remove_var("VIBESQL_AUTH_METHOD");
660        env::remove_var("VIBESQL_LOG_LEVEL");
661        env::remove_var("VIBESQL_HTTP_PORT");
662    }
663
664    #[test]
665    fn test_env_override_http_auth_enabled() {
666        let _lock = ENV_TEST_MUTEX.lock().unwrap();
667        let mut config = Config::default();
668        assert!(!config.http.auth.enabled);
669
670        env::set_var("VIBESQL_HTTP_AUTH_ENABLED", "true");
671        config.apply_env_overrides();
672        assert!(config.http.auth.enabled);
673        env::remove_var("VIBESQL_HTTP_AUTH_ENABLED");
674    }
675
676    #[test]
677    fn test_env_override_http_auth_enabled_false() {
678        let _lock = ENV_TEST_MUTEX.lock().unwrap();
679        let mut config = Config::default();
680        config.http.auth.enabled = true;
681
682        env::set_var("VIBESQL_HTTP_AUTH_ENABLED", "false");
683        config.apply_env_overrides();
684        assert!(!config.http.auth.enabled);
685        env::remove_var("VIBESQL_HTTP_AUTH_ENABLED");
686    }
687
688    #[test]
689    fn test_env_override_http_auth_methods() {
690        let _lock = ENV_TEST_MUTEX.lock().unwrap();
691        let mut config = Config::default();
692        // Default methods are api_key and basic
693        assert_eq!(config.http.auth.methods.len(), 2);
694        assert!(config.http.auth.methods.contains(&HttpAuthMethod::ApiKey));
695        assert!(config.http.auth.methods.contains(&HttpAuthMethod::Basic));
696
697        env::set_var("VIBESQL_HTTP_AUTH_METHODS", "jwt,api_key");
698        config.apply_env_overrides();
699        assert_eq!(config.http.auth.methods.len(), 2);
700        assert!(config.http.auth.methods.contains(&HttpAuthMethod::Jwt));
701        assert!(config.http.auth.methods.contains(&HttpAuthMethod::ApiKey));
702        assert!(!config.http.auth.methods.contains(&HttpAuthMethod::Basic));
703        env::remove_var("VIBESQL_HTTP_AUTH_METHODS");
704    }
705
706    #[test]
707    fn test_env_override_http_auth_methods_case_insensitive() {
708        let _lock = ENV_TEST_MUTEX.lock().unwrap();
709        let mut config = Config::default();
710
711        env::set_var("VIBESQL_HTTP_AUTH_METHODS", "JWT, API_KEY, BASIC");
712        config.apply_env_overrides();
713        assert_eq!(config.http.auth.methods.len(), 3);
714        assert!(config.http.auth.methods.contains(&HttpAuthMethod::Jwt));
715        assert!(config.http.auth.methods.contains(&HttpAuthMethod::ApiKey));
716        assert!(config.http.auth.methods.contains(&HttpAuthMethod::Basic));
717        env::remove_var("VIBESQL_HTTP_AUTH_METHODS");
718    }
719
720    #[test]
721    fn test_env_override_http_auth_methods_invalid_ignored() {
722        let _lock = ENV_TEST_MUTEX.lock().unwrap();
723        let mut config = Config::default();
724
725        // Invalid methods should be silently ignored
726        env::set_var("VIBESQL_HTTP_AUTH_METHODS", "jwt,invalid_method,basic,unknown");
727        config.apply_env_overrides();
728        assert_eq!(config.http.auth.methods.len(), 2);
729        assert!(config.http.auth.methods.contains(&HttpAuthMethod::Jwt));
730        assert!(config.http.auth.methods.contains(&HttpAuthMethod::Basic));
731        env::remove_var("VIBESQL_HTTP_AUTH_METHODS");
732    }
733
734    #[test]
735    fn test_env_override_http_auth_methods_empty_preserves_default() {
736        let _lock = ENV_TEST_MUTEX.lock().unwrap();
737        let mut config = Config::default();
738        let original_methods = config.http.auth.methods.clone();
739
740        // Empty value should preserve default
741        env::set_var("VIBESQL_HTTP_AUTH_METHODS", "");
742        config.apply_env_overrides();
743        assert_eq!(config.http.auth.methods, original_methods);
744        env::remove_var("VIBESQL_HTTP_AUTH_METHODS");
745    }
746
747    #[test]
748    fn test_env_override_http_auth_methods_all_invalid_preserves_default() {
749        let _lock = ENV_TEST_MUTEX.lock().unwrap();
750        let mut config = Config::default();
751        let original_methods = config.http.auth.methods.clone();
752
753        // All invalid values should preserve default
754        env::set_var("VIBESQL_HTTP_AUTH_METHODS", "invalid,unknown,bad");
755        config.apply_env_overrides();
756        assert_eq!(config.http.auth.methods, original_methods);
757        env::remove_var("VIBESQL_HTTP_AUTH_METHODS");
758    }
759
760    #[test]
761    fn test_env_override_http_auth_api_keys() {
762        let _lock = ENV_TEST_MUTEX.lock().unwrap();
763        let mut config = Config::default();
764        assert!(config.http.auth.api_keys.keys.is_empty());
765
766        env::set_var("VIBESQL_HTTP_AUTH_API_KEYS", "key1,key2,key3");
767        config.apply_env_overrides();
768        assert_eq!(config.http.auth.api_keys.keys.len(), 3);
769        assert!(config.http.auth.api_keys.keys.contains(&"key1".to_string()));
770        assert!(config.http.auth.api_keys.keys.contains(&"key2".to_string()));
771        assert!(config.http.auth.api_keys.keys.contains(&"key3".to_string()));
772        env::remove_var("VIBESQL_HTTP_AUTH_API_KEYS");
773    }
774
775    #[test]
776    fn test_env_override_http_auth_api_keys_trims_whitespace() {
777        let _lock = ENV_TEST_MUTEX.lock().unwrap();
778        let mut config = Config::default();
779
780        env::set_var("VIBESQL_HTTP_AUTH_API_KEYS", " key1 , key2 , key3 ");
781        config.apply_env_overrides();
782        assert_eq!(config.http.auth.api_keys.keys.len(), 3);
783        assert!(config.http.auth.api_keys.keys.contains(&"key1".to_string()));
784        assert!(config.http.auth.api_keys.keys.contains(&"key2".to_string()));
785        assert!(config.http.auth.api_keys.keys.contains(&"key3".to_string()));
786        env::remove_var("VIBESQL_HTTP_AUTH_API_KEYS");
787    }
788
789    #[test]
790    fn test_env_override_http_auth_api_keys_empty_values_ignored() {
791        let _lock = ENV_TEST_MUTEX.lock().unwrap();
792        let mut config = Config::default();
793
794        // Empty values between commas should be ignored
795        env::set_var("VIBESQL_HTTP_AUTH_API_KEYS", "key1,,key2,  ,key3");
796        config.apply_env_overrides();
797        assert_eq!(config.http.auth.api_keys.keys.len(), 3);
798        assert!(config.http.auth.api_keys.keys.contains(&"key1".to_string()));
799        assert!(config.http.auth.api_keys.keys.contains(&"key2".to_string()));
800        assert!(config.http.auth.api_keys.keys.contains(&"key3".to_string()));
801        env::remove_var("VIBESQL_HTTP_AUTH_API_KEYS");
802    }
803
804    #[test]
805    fn test_env_override_http_auth_api_keys_empty_preserves_default() {
806        let _lock = ENV_TEST_MUTEX.lock().unwrap();
807        let mut config = Config::default();
808        // Pre-populate with a key
809        config.http.auth.api_keys.keys = vec!["original-key".to_string()];
810
811        // Empty value should preserve existing keys
812        env::set_var("VIBESQL_HTTP_AUTH_API_KEYS", "");
813        config.apply_env_overrides();
814        assert_eq!(config.http.auth.api_keys.keys, vec!["original-key".to_string()]);
815        env::remove_var("VIBESQL_HTTP_AUTH_API_KEYS");
816    }
817
818    #[test]
819    fn test_env_override_http_auth_api_keys_single_key() {
820        let _lock = ENV_TEST_MUTEX.lock().unwrap();
821        let mut config = Config::default();
822
823        env::set_var("VIBESQL_HTTP_AUTH_API_KEYS", "single-key");
824        config.apply_env_overrides();
825        assert_eq!(config.http.auth.api_keys.keys.len(), 1);
826        assert_eq!(config.http.auth.api_keys.keys[0], "single-key");
827        env::remove_var("VIBESQL_HTTP_AUTH_API_KEYS");
828    }
829
830    #[test]
831    fn test_env_override_jwt_secret() {
832        let _lock = ENV_TEST_MUTEX.lock().unwrap();
833        let mut config = Config::default();
834        assert_eq!(config.http.auth.jwt.secret, "");
835
836        env::set_var("VIBESQL_HTTP_AUTH_JWT_SECRET", "my-super-secret-key");
837        config.apply_env_overrides();
838        assert_eq!(config.http.auth.jwt.secret, "my-super-secret-key");
839        env::remove_var("VIBESQL_HTTP_AUTH_JWT_SECRET");
840    }
841
842    #[test]
843    fn test_env_override_jwt_issuer() {
844        let _lock = ENV_TEST_MUTEX.lock().unwrap();
845        let mut config = Config::default();
846        assert_eq!(config.http.auth.jwt.issuer, Some("vibesql".to_string()));
847
848        env::set_var("VIBESQL_HTTP_AUTH_JWT_ISSUER", "custom-issuer");
849        config.apply_env_overrides();
850        assert_eq!(config.http.auth.jwt.issuer, Some("custom-issuer".to_string()));
851        env::remove_var("VIBESQL_HTTP_AUTH_JWT_ISSUER");
852    }
853
854    #[test]
855    fn test_env_override_jwt_audience() {
856        let _lock = ENV_TEST_MUTEX.lock().unwrap();
857        let mut config = Config::default();
858        assert_eq!(config.http.auth.jwt.audience, Some("vibesql-api".to_string()));
859
860        env::set_var("VIBESQL_HTTP_AUTH_JWT_AUDIENCE", "custom-audience");
861        config.apply_env_overrides();
862        assert_eq!(config.http.auth.jwt.audience, Some("custom-audience".to_string()));
863        env::remove_var("VIBESQL_HTTP_AUTH_JWT_AUDIENCE");
864    }
865
866    #[test]
867    fn test_env_override_jwt_expiration() {
868        let _lock = ENV_TEST_MUTEX.lock().unwrap();
869        let mut config = Config::default();
870        assert_eq!(config.http.auth.jwt.expiration_secs, 3600);
871
872        env::set_var("VIBESQL_HTTP_AUTH_JWT_EXPIRATION", "7200");
873        config.apply_env_overrides();
874        assert_eq!(config.http.auth.jwt.expiration_secs, 7200);
875        env::remove_var("VIBESQL_HTTP_AUTH_JWT_EXPIRATION");
876    }
877
878    #[test]
879    fn test_env_override_jwt_expiration_invalid() {
880        let _lock = ENV_TEST_MUTEX.lock().unwrap();
881        let mut config = Config::default();
882        assert_eq!(config.http.auth.jwt.expiration_secs, 3600);
883
884        // Invalid expiration should be ignored
885        env::set_var("VIBESQL_HTTP_AUTH_JWT_EXPIRATION", "not_a_number");
886        config.apply_env_overrides();
887        assert_eq!(config.http.auth.jwt.expiration_secs, 3600);
888        env::remove_var("VIBESQL_HTTP_AUTH_JWT_EXPIRATION");
889    }
890}