1use crate::{ProxyError, Result};
6use serde::{Deserialize, Serialize};
7use std::path::Path;
8use std::time::Duration;
9
10#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
18#[serde(rename_all = "lowercase")]
19pub enum PoolingMode {
20 #[default]
22 Session,
23 Transaction,
25 Statement,
27}
28
29#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
31#[serde(rename_all = "lowercase")]
32pub enum PreparedStatementMode {
33 #[default]
35 Disable,
36 Track,
38 Named,
40}
41
42#[derive(Debug, Clone, Serialize, Deserialize)]
44pub struct PoolModeConfig {
45 #[serde(default)]
47 pub mode: PoolingMode,
48 #[serde(default = "default_pool_mode_max_size")]
50 pub max_pool_size: u32,
51 #[serde(default = "default_pool_mode_min_idle")]
53 pub min_idle: u32,
54 #[serde(default = "default_pool_mode_idle_timeout")]
56 pub idle_timeout_secs: u64,
57 #[serde(default = "default_pool_mode_max_lifetime")]
59 pub max_lifetime_secs: u64,
60 #[serde(default = "default_pool_mode_acquire_timeout")]
62 pub acquire_timeout_secs: u64,
63 #[serde(default = "default_reset_query")]
65 pub reset_query: String,
66 #[serde(default)]
68 pub prepared_statement_mode: PreparedStatementMode,
69}
70
71fn default_pool_mode_max_size() -> u32 {
72 100
73}
74
75fn default_pool_mode_min_idle() -> u32 {
76 10
77}
78
79fn default_pool_mode_idle_timeout() -> u64 {
80 600
81}
82
83fn default_pool_mode_max_lifetime() -> u64 {
84 3600
85}
86
87fn default_pool_mode_acquire_timeout() -> u64 {
88 5
89}
90
91fn default_reset_query() -> String {
92 "DISCARD ALL".to_string()
93}
94
95impl Default for PoolModeConfig {
96 fn default() -> Self {
97 Self {
98 mode: PoolingMode::default(),
99 max_pool_size: default_pool_mode_max_size(),
100 min_idle: default_pool_mode_min_idle(),
101 idle_timeout_secs: default_pool_mode_idle_timeout(),
102 max_lifetime_secs: default_pool_mode_max_lifetime(),
103 acquire_timeout_secs: default_pool_mode_acquire_timeout(),
104 reset_query: default_reset_query(),
105 prepared_statement_mode: PreparedStatementMode::default(),
106 }
107 }
108}
109
110impl PoolModeConfig {
111 pub fn session_mode() -> Self {
113 Self {
114 mode: PoolingMode::Session,
115 prepared_statement_mode: PreparedStatementMode::Named,
116 ..Default::default()
117 }
118 }
119
120 pub fn transaction_mode() -> Self {
122 Self {
123 mode: PoolingMode::Transaction,
124 prepared_statement_mode: PreparedStatementMode::Track,
125 ..Default::default()
126 }
127 }
128
129 pub fn statement_mode() -> Self {
131 Self {
132 mode: PoolingMode::Statement,
133 prepared_statement_mode: PreparedStatementMode::Disable,
134 ..Default::default()
135 }
136 }
137
138 pub fn idle_timeout(&self) -> Duration {
140 Duration::from_secs(self.idle_timeout_secs)
141 }
142
143 pub fn max_lifetime(&self) -> Duration {
145 Duration::from_secs(self.max_lifetime_secs)
146 }
147
148 pub fn acquire_timeout(&self) -> Duration {
150 Duration::from_secs(self.acquire_timeout_secs)
151 }
152}
153
154#[derive(Debug, Clone, Serialize, Deserialize)]
160pub struct ProxyConfig {
161 pub listen_address: String,
163 pub admin_address: String,
165 #[serde(default)]
170 pub admin_token: Option<String>,
171 pub tr_enabled: bool,
173 pub tr_mode: TrMode,
175 pub pool: PoolConfig,
177 #[serde(default)]
179 pub pool_mode: PoolModeConfig,
180 pub load_balancer: LoadBalancerConfig,
182 pub health: HealthConfig,
184 pub nodes: Vec<NodeConfig>,
186 pub tls: Option<TlsConfig>,
188 #[serde(default = "default_write_timeout_secs")]
191 pub write_timeout_secs: u64,
192 #[serde(default)]
196 pub plugins: PluginToml,
197 #[serde(default)]
201 pub hba: Vec<HbaRule>,
202 #[serde(default)]
205 pub auth: AuthConfig,
206 #[serde(default)]
208 pub mcp: McpConfig,
209 #[serde(default)]
212 pub agent_contracts: Vec<crate::agent_contract::AgentContract>,
213 #[serde(default)]
216 pub http_gateway: HttpGatewayConfig,
217 #[serde(default)]
220 pub mirror: MirrorConfig,
221 #[serde(default)]
224 pub branch: BranchConfig,
225 #[serde(default = "default_true")]
232 pub optimize_unnamed_parse: bool,
233 #[serde(default = "default_drain_timeout_secs")]
239 pub shutdown_drain_timeout_secs: u64,
240}
241
242fn default_drain_timeout_secs() -> u64 {
243 60
244}
245
246#[derive(Debug, Clone, Serialize, Deserialize)]
249pub struct BranchConfig {
250 #[serde(default)]
251 pub enabled: bool,
252 #[serde(default = "default_localhost")]
253 pub backend_host: String,
254 #[serde(default = "default_pg_port")]
255 pub backend_port: u16,
256 #[serde(default = "default_pg_user")]
258 pub admin_user: String,
259 pub admin_password: Option<String>,
260 #[serde(default = "default_admin_db")]
263 pub admin_database: String,
264 #[serde(default = "default_admin_db")]
266 pub base_database: String,
267}
268
269impl Default for BranchConfig {
270 fn default() -> Self {
271 Self {
272 enabled: false,
273 backend_host: default_localhost(),
274 backend_port: default_pg_port(),
275 admin_user: default_pg_user(),
276 admin_password: None,
277 admin_database: default_admin_db(),
278 base_database: default_admin_db(),
279 }
280 }
281}
282
283fn default_admin_db() -> String {
284 "postgres".to_string()
285}
286
287#[derive(Debug, Clone, Serialize, Deserialize)]
290pub struct MirrorConfig {
291 #[serde(default)]
292 pub enabled: bool,
293 #[serde(default = "default_sample_rate")]
295 pub sample_rate: f64,
296 #[serde(default = "default_true_bool")]
299 pub writes_only: bool,
300 #[serde(default = "default_mirror_queue")]
303 pub queue_size: usize,
304 #[serde(default = "default_localhost")]
305 pub backend_host: String,
306 #[serde(default = "default_pg_port")]
307 pub backend_port: u16,
308 #[serde(default = "default_pg_user")]
309 pub backend_user: String,
310 pub backend_password: Option<String>,
311 pub backend_database: Option<String>,
312 #[serde(default = "default_localhost")]
316 pub source_host: String,
317 #[serde(default = "default_pg_port")]
318 pub source_port: u16,
319 #[serde(default = "default_pg_user")]
320 pub source_user: String,
321 pub source_password: Option<String>,
322 pub source_database: Option<String>,
323}
324
325impl Default for MirrorConfig {
326 fn default() -> Self {
327 Self {
328 enabled: false,
329 sample_rate: 1.0,
330 writes_only: true,
331 queue_size: 10_000,
332 backend_host: default_localhost(),
333 backend_port: default_pg_port(),
334 backend_user: default_pg_user(),
335 backend_password: None,
336 backend_database: None,
337 source_host: default_localhost(),
338 source_port: default_pg_port(),
339 source_user: default_pg_user(),
340 source_password: None,
341 source_database: None,
342 }
343 }
344}
345
346fn default_sample_rate() -> f64 {
347 1.0
348}
349fn default_mirror_queue() -> usize {
350 10_000
351}
352
353#[derive(Debug, Clone, Serialize, Deserialize)]
357pub struct HttpGatewayConfig {
358 #[serde(default)]
359 pub enabled: bool,
360 #[serde(default = "default_http_gw_listen")]
361 pub listen_address: String,
362 #[serde(default = "default_localhost")]
363 pub backend_host: String,
364 #[serde(default = "default_pg_port")]
365 pub backend_port: u16,
366 #[serde(default = "default_pg_user")]
367 pub backend_user: String,
368 pub backend_password: Option<String>,
369 pub backend_database: Option<String>,
370 #[serde(default)]
372 pub auth_token: Option<String>,
373}
374
375impl Default for HttpGatewayConfig {
376 fn default() -> Self {
377 Self {
378 enabled: false,
379 listen_address: default_http_gw_listen(),
380 backend_host: default_localhost(),
381 backend_port: default_pg_port(),
382 backend_user: default_pg_user(),
383 backend_password: None,
384 backend_database: None,
385 auth_token: None,
386 }
387 }
388}
389
390fn default_http_gw_listen() -> String {
391 "127.0.0.1:9093".to_string()
392}
393
394#[derive(Debug, Clone, Serialize, Deserialize)]
399pub struct McpConfig {
400 #[serde(default)]
401 pub enabled: bool,
402 #[serde(default = "default_mcp_listen")]
404 pub listen_address: String,
405 #[serde(default = "default_localhost")]
407 pub backend_host: String,
408 #[serde(default = "default_pg_port")]
409 pub backend_port: u16,
410 #[serde(default = "default_pg_user")]
411 pub backend_user: String,
412 pub backend_password: Option<String>,
413 pub backend_database: Option<String>,
414 #[serde(default = "default_true_bool")]
417 pub read_only: bool,
418 #[serde(default)]
421 pub contract: Option<String>,
422}
423
424impl Default for McpConfig {
425 fn default() -> Self {
426 Self {
427 enabled: false,
428 listen_address: default_mcp_listen(),
429 backend_host: default_localhost(),
430 backend_port: default_pg_port(),
431 backend_user: default_pg_user(),
432 backend_password: None,
433 backend_database: None,
434 read_only: true,
435 contract: None,
436 }
437 }
438}
439
440fn default_mcp_listen() -> String {
441 "127.0.0.1:9092".to_string()
442}
443fn default_localhost() -> String {
444 "127.0.0.1".to_string()
445}
446fn default_pg_port() -> u16 {
447 5432
448}
449fn default_pg_user() -> String {
450 "postgres".to_string()
451}
452fn default_true_bool() -> bool {
453 true
454}
455
456#[derive(Debug, Clone, Serialize, Deserialize, Default)]
458pub struct AuthConfig {
459 #[serde(default)]
463 pub mode: AuthMode,
464 #[serde(default)]
467 pub auth_file: Option<String>,
468}
469
470#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
472#[serde(rename_all = "lowercase")]
473pub enum AuthMode {
474 #[default]
476 Passthrough,
477 Scram,
479}
480
481#[derive(Debug, Clone, Serialize, Deserialize)]
488pub struct HbaRule {
489 pub action: HbaAction,
491 #[serde(default = "hba_all")]
493 pub user: String,
494 #[serde(default = "hba_all")]
496 pub database: String,
497 #[serde(default = "hba_all")]
500 pub address: String,
501}
502
503fn hba_all() -> String {
504 "all".to_string()
505}
506
507#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
509#[serde(rename_all = "lowercase")]
510pub enum HbaAction {
511 Allow,
512 Reject,
513}
514
515fn default_write_timeout_secs() -> u64 {
516 30 }
518
519impl Default for ProxyConfig {
520 fn default() -> Self {
521 Self {
522 listen_address: "0.0.0.0:5432".to_string(),
523 admin_address: "0.0.0.0:9090".to_string(),
524 admin_token: None,
525 tr_enabled: true,
526 tr_mode: TrMode::Session,
527 pool: PoolConfig::default(),
528 pool_mode: PoolModeConfig::default(),
529 load_balancer: LoadBalancerConfig::default(),
530 health: HealthConfig::default(),
531 nodes: Vec::new(),
532 tls: None,
533 write_timeout_secs: default_write_timeout_secs(),
534 plugins: PluginToml::default(),
535 hba: Vec::new(),
536 auth: AuthConfig::default(),
537 mcp: McpConfig::default(),
538 agent_contracts: Vec::new(),
539 http_gateway: HttpGatewayConfig::default(),
540 mirror: MirrorConfig::default(),
541 branch: BranchConfig::default(),
542 optimize_unnamed_parse: true,
543 shutdown_drain_timeout_secs: default_drain_timeout_secs(),
544 }
545 }
546}
547
548#[derive(Debug, Clone, Serialize, Deserialize)]
562pub struct PluginToml {
563 #[serde(default)]
566 pub enabled: bool,
567 #[serde(default = "default_plugin_dir")]
569 pub plugin_dir: String,
570 #[serde(default)]
572 pub hot_reload: bool,
573 #[serde(default = "default_plugin_memory_mb")]
575 pub memory_limit_mb: usize,
576 #[serde(default = "default_plugin_timeout_ms")]
578 pub timeout_ms: u64,
579 #[serde(default = "default_plugin_max")]
581 pub max_plugins: usize,
582 #[serde(default = "default_true")]
584 pub fuel_metering: bool,
585 #[serde(default = "default_plugin_fuel")]
587 pub fuel_limit: u64,
588 #[serde(default)]
594 pub trust_root: Option<String>,
595}
596
597fn default_plugin_dir() -> String {
598 "/etc/heliosproxy/plugins".to_string()
599}
600fn default_plugin_memory_mb() -> usize {
601 64
602}
603fn default_plugin_timeout_ms() -> u64 {
604 100
605}
606fn default_plugin_max() -> usize {
607 20
608}
609fn default_true() -> bool {
610 true
611}
612fn default_plugin_fuel() -> u64 {
613 1_000_000
614}
615
616impl Default for PluginToml {
617 fn default() -> Self {
618 Self {
619 enabled: false,
620 plugin_dir: default_plugin_dir(),
621 hot_reload: false,
622 memory_limit_mb: default_plugin_memory_mb(),
623 timeout_ms: default_plugin_timeout_ms(),
624 max_plugins: default_plugin_max(),
625 fuel_metering: true,
626 fuel_limit: default_plugin_fuel(),
627 trust_root: None,
628 }
629 }
630}
631
632impl ProxyConfig {
633 pub fn write_timeout(&self) -> Duration {
635 Duration::from_secs(self.write_timeout_secs)
636 }
637
638 pub fn from_file(path: &str) -> Result<Self> {
640 let path = Path::new(path);
641
642 if !path.exists() {
643 return Err(ProxyError::Config(format!(
644 "Configuration file not found: {}",
645 path.display()
646 )));
647 }
648
649 let contents = std::fs::read_to_string(path)
650 .map_err(|e| ProxyError::Config(format!("Failed to read config: {}", e)))?;
651
652 let config: Self = toml::from_str(&contents)
653 .map_err(|e| ProxyError::Config(format!("Failed to parse config: {}", e)))?;
654
655 config.validate()?;
656
657 Ok(config)
658 }
659
660 pub fn add_node(&mut self, host_port: &str, role: &str) -> Result<()> {
662 let parts: Vec<&str> = host_port.rsplitn(2, ':').collect();
663 if parts.len() != 2 {
664 return Err(ProxyError::Config(format!(
665 "Invalid host:port format: {}",
666 host_port
667 )));
668 }
669
670 let port: u16 = parts[0].parse()
671 .map_err(|_| ProxyError::Config(format!("Invalid port: {}", parts[0])))?;
672
673 let host = parts[1].to_string();
674
675 let role = match role {
676 "primary" => NodeRole::Primary,
677 "standby" => NodeRole::Standby,
678 "replica" => NodeRole::ReadReplica,
679 _ => return Err(ProxyError::Config(format!("Unknown role: {}", role))),
680 };
681
682 self.nodes.push(NodeConfig {
683 host,
684 port,
685 http_port: default_http_port(),
686 role,
687 weight: 100,
688 enabled: true,
689 name: None,
690 });
691
692 Ok(())
693 }
694
695 pub fn validate(&self) -> Result<()> {
697 if self.nodes.is_empty() {
699 return Err(ProxyError::Config("No backend nodes configured".to_string()));
700 }
701
702 let has_primary = self.nodes.iter().any(|n| n.role == NodeRole::Primary);
704 if !has_primary {
705 return Err(ProxyError::Config("No primary node configured".to_string()));
706 }
707
708 if self.pool.max_connections < self.pool.min_connections {
710 return Err(ProxyError::Config(
711 "max_connections must be >= min_connections".to_string(),
712 ));
713 }
714
715 Ok(())
716 }
717
718 pub fn primary_node(&self) -> Option<&NodeConfig> {
720 self.nodes.iter().find(|n| n.role == NodeRole::Primary && n.enabled)
721 }
722
723 pub fn standby_nodes(&self) -> Vec<&NodeConfig> {
725 self.nodes.iter()
726 .filter(|n| n.role == NodeRole::Standby && n.enabled)
727 .collect()
728 }
729
730 pub fn enabled_nodes(&self) -> Vec<&NodeConfig> {
732 self.nodes.iter().filter(|n| n.enabled).collect()
733 }
734}
735
736#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
738#[serde(rename_all = "lowercase")]
739pub enum TrMode {
740 None,
742 Session,
744 Select,
746 Transaction,
748}
749
750impl Default for TrMode {
751 fn default() -> Self {
752 TrMode::Session
753 }
754}
755
756#[derive(Debug, Clone, Serialize, Deserialize)]
758pub struct PoolConfig {
759 pub min_connections: usize,
761 pub max_connections: usize,
763 pub idle_timeout_secs: u64,
765 pub max_lifetime_secs: u64,
767 pub acquire_timeout_secs: u64,
769 pub test_on_acquire: bool,
771}
772
773impl Default for PoolConfig {
774 fn default() -> Self {
775 Self {
776 min_connections: 2,
777 max_connections: 100,
778 idle_timeout_secs: 300,
779 max_lifetime_secs: 1800,
780 acquire_timeout_secs: 30,
781 test_on_acquire: true,
782 }
783 }
784}
785
786impl PoolConfig {
787 pub fn idle_timeout(&self) -> Duration {
789 Duration::from_secs(self.idle_timeout_secs)
790 }
791
792 pub fn max_lifetime(&self) -> Duration {
794 Duration::from_secs(self.max_lifetime_secs)
795 }
796
797 pub fn acquire_timeout(&self) -> Duration {
799 Duration::from_secs(self.acquire_timeout_secs)
800 }
801}
802
803#[derive(Debug, Clone, Serialize, Deserialize)]
805pub struct LoadBalancerConfig {
806 pub read_strategy: Strategy,
808 pub read_write_split: bool,
810 pub latency_threshold_ms: u64,
812}
813
814impl Default for LoadBalancerConfig {
815 fn default() -> Self {
816 Self {
817 read_strategy: Strategy::RoundRobin,
818 read_write_split: true,
819 latency_threshold_ms: 100,
820 }
821 }
822}
823
824#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
826#[serde(rename_all = "snake_case")]
827pub enum Strategy {
828 RoundRobin,
830 WeightedRoundRobin,
832 LeastConnections,
834 LatencyBased,
836 Random,
838}
839
840#[derive(Debug, Clone, Serialize, Deserialize)]
842pub struct HealthConfig {
843 pub check_interval_secs: u64,
845 pub check_timeout_secs: u64,
847 pub failure_threshold: u32,
849 pub success_threshold: u32,
851 pub check_query: String,
853}
854
855impl Default for HealthConfig {
856 fn default() -> Self {
857 Self {
858 check_interval_secs: 5,
859 check_timeout_secs: 3,
860 failure_threshold: 3,
861 success_threshold: 2,
862 check_query: "SELECT 1".to_string(),
863 }
864 }
865}
866
867impl HealthConfig {
868 pub fn check_interval(&self) -> Duration {
870 Duration::from_secs(self.check_interval_secs)
871 }
872
873 pub fn check_timeout(&self) -> Duration {
875 Duration::from_secs(self.check_timeout_secs)
876 }
877}
878
879#[derive(Debug, Clone, Serialize, Deserialize)]
881pub struct NodeConfig {
882 pub host: String,
884 pub port: u16,
886 #[serde(default = "default_http_port")]
889 pub http_port: u16,
890 pub role: NodeRole,
892 pub weight: u32,
894 pub enabled: bool,
896 pub name: Option<String>,
898}
899
900fn default_http_port() -> u16 {
901 8080
902}
903
904impl NodeConfig {
905 pub fn address(&self) -> String {
907 format!("{}:{}", self.host, self.port)
908 }
909
910 pub fn display_name(&self) -> &str {
912 self.name.as_deref().unwrap_or(&self.host)
913 }
914}
915
916#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
918#[serde(rename_all = "lowercase")]
919pub enum NodeRole {
920 Primary,
922 Standby,
924 #[serde(rename = "replica")]
926 ReadReplica,
927}
928
929#[derive(Debug, Clone, Serialize, Deserialize)]
931pub struct TlsConfig {
932 pub enabled: bool,
934 pub cert_path: String,
936 pub key_path: String,
938 pub ca_path: Option<String>,
940 pub require_client_cert: bool,
942}
943
944#[cfg(test)]
945mod tests {
946 use super::*;
947
948 #[test]
949 fn test_default_config() {
950 let config = ProxyConfig::default();
951 assert_eq!(config.listen_address, "0.0.0.0:5432");
952 assert!(config.tr_enabled);
953 }
954
955 #[test]
956 fn test_add_node() {
957 let mut config = ProxyConfig::default();
958 config.add_node("localhost:5432", "primary").unwrap();
959 config.add_node("localhost:5433", "standby").unwrap();
960
961 assert_eq!(config.nodes.len(), 2);
962 assert!(config.primary_node().is_some());
963 assert_eq!(config.standby_nodes().len(), 1);
964 }
965
966 #[test]
967 fn test_validate_no_nodes() {
968 let config = ProxyConfig::default();
969 assert!(config.validate().is_err());
970 }
971
972 #[test]
973 fn test_validate_no_primary() {
974 let mut config = ProxyConfig::default();
975 config.add_node("localhost:5432", "standby").unwrap();
976 assert!(config.validate().is_err());
977 }
978
979 #[test]
980 fn test_validate_success() {
981 let mut config = ProxyConfig::default();
982 config.add_node("localhost:5432", "primary").unwrap();
983 assert!(config.validate().is_ok());
984 }
985
986 #[test]
987 fn test_pool_config_durations() {
988 let config = PoolConfig::default();
989 assert_eq!(config.idle_timeout(), Duration::from_secs(300));
990 assert_eq!(config.max_lifetime(), Duration::from_secs(1800));
991 }
992
993 #[test]
994 fn test_pool_mode_default() {
995 let config = PoolModeConfig::default();
996 assert_eq!(config.mode, PoolingMode::Session);
997 assert_eq!(config.max_pool_size, 100);
998 assert_eq!(config.min_idle, 10);
999 assert_eq!(config.reset_query, "DISCARD ALL");
1000 }
1001
1002 #[test]
1003 fn test_pool_mode_session() {
1004 let config = PoolModeConfig::session_mode();
1005 assert_eq!(config.mode, PoolingMode::Session);
1006 assert_eq!(config.prepared_statement_mode, PreparedStatementMode::Named);
1007 }
1008
1009 #[test]
1010 fn test_pool_mode_transaction() {
1011 let config = PoolModeConfig::transaction_mode();
1012 assert_eq!(config.mode, PoolingMode::Transaction);
1013 assert_eq!(config.prepared_statement_mode, PreparedStatementMode::Track);
1014 }
1015
1016 #[test]
1017 fn test_pool_mode_statement() {
1018 let config = PoolModeConfig::statement_mode();
1019 assert_eq!(config.mode, PoolingMode::Statement);
1020 assert_eq!(config.prepared_statement_mode, PreparedStatementMode::Disable);
1021 }
1022
1023 #[test]
1024 fn test_pool_mode_durations() {
1025 let config = PoolModeConfig::default();
1026 assert_eq!(config.idle_timeout(), Duration::from_secs(600));
1027 assert_eq!(config.max_lifetime(), Duration::from_secs(3600));
1028 assert_eq!(config.acquire_timeout(), Duration::from_secs(5));
1029 }
1030
1031 #[test]
1032 fn test_proxy_config_has_pool_mode() {
1033 let config = ProxyConfig::default();
1034 assert_eq!(config.pool_mode.mode, PoolingMode::Session);
1035 }
1036
1037 #[test]
1041 fn test_plugin_toml_default_is_disabled() {
1042 let config = ProxyConfig::default();
1043 assert!(!config.plugins.enabled);
1044 assert_eq!(config.plugins.plugin_dir, "/etc/heliosproxy/plugins");
1045 assert_eq!(config.plugins.memory_limit_mb, 64);
1046 assert_eq!(config.plugins.timeout_ms, 100);
1047 }
1048
1049 #[test]
1053 fn test_proxy_config_toml_without_plugins_section_still_parses() {
1054 let toml_text = r#"
1055 listen_address = "0.0.0.0:5432"
1056 admin_address = "0.0.0.0:9090"
1057 tr_enabled = true
1058 tr_mode = "session"
1059 nodes = []
1060
1061 [pool]
1062 min_connections = 2
1063 max_connections = 10
1064 idle_timeout_secs = 300
1065 max_lifetime_secs = 1800
1066 acquire_timeout_secs = 30
1067 test_on_acquire = true
1068
1069 [load_balancer]
1070 read_strategy = "round_robin"
1071 read_write_split = true
1072 latency_threshold_ms = 100
1073
1074 [health]
1075 check_interval_secs = 5
1076 check_timeout_secs = 3
1077 failure_threshold = 3
1078 success_threshold = 2
1079 check_query = "SELECT 1"
1080 "#;
1081 let config: ProxyConfig = toml::from_str(toml_text).expect("parse");
1082 assert!(!config.plugins.enabled);
1083 }
1084
1085 #[test]
1088 fn test_plugin_toml_overrides_parse() {
1089 let toml_text = r#"
1090 listen_address = "0.0.0.0:5432"
1091 admin_address = "0.0.0.0:9090"
1092 tr_enabled = true
1093 tr_mode = "session"
1094 nodes = []
1095
1096 [pool]
1097 min_connections = 2
1098 max_connections = 10
1099 idle_timeout_secs = 300
1100 max_lifetime_secs = 1800
1101 acquire_timeout_secs = 30
1102 test_on_acquire = true
1103
1104 [load_balancer]
1105 read_strategy = "round_robin"
1106 read_write_split = true
1107 latency_threshold_ms = 100
1108
1109 [health]
1110 check_interval_secs = 5
1111 check_timeout_secs = 3
1112 failure_threshold = 3
1113 success_threshold = 2
1114 check_query = "SELECT 1"
1115
1116 [plugins]
1117 enabled = true
1118 plugin_dir = "/tmp/helios-plugins"
1119 hot_reload = true
1120 memory_limit_mb = 128
1121 timeout_ms = 250
1122 "#;
1123 let config: ProxyConfig = toml::from_str(toml_text).expect("parse");
1124 assert!(config.plugins.enabled);
1125 assert_eq!(config.plugins.plugin_dir, "/tmp/helios-plugins");
1126 assert!(config.plugins.hot_reload);
1127 assert_eq!(config.plugins.memory_limit_mb, 128);
1128 assert_eq!(config.plugins.timeout_ms, 250);
1129 assert_eq!(config.plugins.max_plugins, 20);
1131 assert!(config.plugins.fuel_metering);
1132 }
1133}