Skip to main content

nntp_proxy/config/
validation.rs

1//! Configuration validation
2//!
3//! This module provides validation logic for the configuration to ensure
4//! all settings are valid before the proxy starts.
5
6use anyhow::Result;
7use std::time::Duration;
8
9use super::types::{Config, Server};
10use crate::constants::pool::{MAX_RECOMMENDED_KEEPALIVE_SECS, MIN_RECOMMENDED_KEEPALIVE_SECS};
11
12const MIN_RECOMMENDED_KEEPALIVE: Duration = Duration::from_secs(MIN_RECOMMENDED_KEEPALIVE_SECS);
13const MAX_RECOMMENDED_KEEPALIVE: Duration = Duration::from_secs(MAX_RECOMMENDED_KEEPALIVE_SECS);
14
15impl Config {
16    /// Validate configuration for correctness
17    ///
18    /// Most validations are now enforced by type system (NonZero types, validated strings, etc.)
19    /// This checks remaining semantic constraints:
20    /// - At least one server configured
21    /// - Maximum 8 servers (bitset limitation for article availability tracking)
22    /// - Keep-alive intervals are in recommended ranges
23    pub fn validate(&self) -> Result<()> {
24        if self.servers.is_empty() {
25            return Err(anyhow::anyhow!(
26                "Configuration must have at least one server"
27            ));
28        }
29
30        if self.servers.len() > 8 {
31            return Err(anyhow::anyhow!(
32                "Configuration cannot have more than 8 servers (current limitation: u8 bitset for article availability tracking). \
33                 Found {} servers. Consider running multiple proxy instances or file an issue if you need more backends.",
34                self.servers.len()
35            ));
36        }
37
38        for server in &self.servers {
39            validate_server(server)?;
40        }
41
42        Ok(())
43    }
44}
45
46/// Validate a single server configuration
47fn validate_server(server: &Server) -> Result<()> {
48    // Name, host, port, max_connections validations now enforced by types:
49    // - HostName/ServerName cannot be empty (validated at construction)
50    // - Port cannot be 0 (NonZeroU16)
51    // - max_connections cannot be 0 (NonZeroUsize via MaxConnections)
52
53    // Warn if connection_keepalive is outside recommended range
54    if let Some(keepalive) = server.connection_keepalive {
55        if keepalive < MIN_RECOMMENDED_KEEPALIVE {
56            tracing::warn!(
57                "Server '{}' has connection_keepalive set to {:?} (< {:?}). \
58                 This may cause excessive health check traffic and connection churn. \
59                 Consider using at least {:?} or None to disable.",
60                server.name.as_str(),
61                keepalive,
62                MIN_RECOMMENDED_KEEPALIVE,
63                MIN_RECOMMENDED_KEEPALIVE
64            );
65        } else if keepalive > MAX_RECOMMENDED_KEEPALIVE {
66            tracing::warn!(
67                "Server '{}' has connection_keepalive set to {:?} (> {:?} / 5 minutes). \
68                 This may not detect stale connections quickly enough. Consider a lower value.",
69                server.name.as_str(),
70                keepalive,
71                MAX_RECOMMENDED_KEEPALIVE
72            );
73        }
74    }
75
76    Ok(())
77}
78
79#[cfg(test)]
80mod tests {
81    use super::*;
82    use crate::types::Port;
83
84    fn create_test_server(name: &str, keepalive: Option<Duration>) -> Server {
85        let mut builder = Server::builder("localhost", Port::try_new(119).unwrap()).name(name);
86
87        if let Some(ka) = keepalive {
88            builder = builder.connection_keepalive(ka);
89        }
90
91        builder.build().unwrap()
92    }
93
94    #[test]
95    fn test_validate_empty_config_fails() {
96        let config = Config {
97            servers: vec![],
98            ..Default::default()
99        };
100        assert!(config.validate().is_err());
101    }
102
103    #[test]
104    fn test_validate_single_server_succeeds() {
105        let config = Config {
106            servers: vec![create_test_server("test", None)],
107            ..Default::default()
108        };
109        assert!(config.validate().is_ok());
110    }
111
112    #[test]
113    fn test_validate_eight_servers_succeeds() {
114        let config = Config {
115            servers: (0..8)
116                .map(|i| create_test_server(&format!("server{}", i), None))
117                .collect(),
118            ..Default::default()
119        };
120        assert!(config.validate().is_ok());
121    }
122
123    #[test]
124    fn test_validate_nine_servers_fails() {
125        let config = Config {
126            servers: (0..9)
127                .map(|i| create_test_server(&format!("server{}", i), None))
128                .collect(),
129            ..Default::default()
130        };
131        let result = config.validate();
132        assert!(result.is_err());
133        assert!(
134            result
135                .unwrap_err()
136                .to_string()
137                .contains("cannot have more than 8 servers")
138        );
139    }
140
141    #[test]
142    fn test_validate_multiple_servers_succeeds() {
143        let config = Config {
144            servers: vec![
145                create_test_server("server1", None),
146                create_test_server("server2", None),
147            ],
148            ..Default::default()
149        };
150        assert!(config.validate().is_ok());
151    }
152
153    #[test]
154    fn test_validate_server_with_recommended_keepalive() {
155        let server = create_test_server("test", Some(Duration::from_secs(60)));
156        assert!(validate_server(&server).is_ok());
157    }
158
159    #[test]
160    fn test_validate_server_with_low_keepalive_warns() {
161        // This should warn but not fail
162        let server = create_test_server("test", Some(Duration::from_secs(5)));
163        assert!(validate_server(&server).is_ok());
164    }
165
166    #[test]
167    fn test_validate_server_with_high_keepalive_warns() {
168        // This should warn but not fail
169        let server = create_test_server("test", Some(Duration::from_secs(600)));
170        assert!(validate_server(&server).is_ok());
171    }
172
173    #[test]
174    fn test_validate_server_with_no_keepalive() {
175        let server = create_test_server("test", None);
176        assert!(validate_server(&server).is_ok());
177    }
178
179    #[test]
180    fn test_validate_server_at_min_boundary() {
181        let server = create_test_server("test", Some(MIN_RECOMMENDED_KEEPALIVE));
182        assert!(validate_server(&server).is_ok());
183    }
184
185    #[test]
186    fn test_validate_server_at_max_boundary() {
187        let server = create_test_server("test", Some(MAX_RECOMMENDED_KEEPALIVE));
188        assert!(validate_server(&server).is_ok());
189    }
190}